Repository: bggRGjQaUbCoE/PiliPlus Branch: main Commit: 662ccfcf0aa3 Files: 1463 Total size: 14.7 MB Directory structure: gitextract_69oby6yj/ ├── .fvmrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-反馈.yml │ │ └── 功能请求.yml │ └── workflows/ │ ├── build.yml │ ├── ios.yml │ ├── linux_x64.yml │ ├── mac.yml │ └── win_x64.yml ├── .gitignore ├── .metadata ├── .vscode/ │ ├── launch.json │ └── settings.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── debug/ │ │ │ └── res/ │ │ │ └── values/ │ │ │ └── string.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── piliplus/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_baseline_forward_10_24.xml │ │ │ │ ├── ic_baseline_replay_10_24.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── ic_notification_icon.xml │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-night/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-night-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ ├── raw/ │ │ │ │ └── keep.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── string.xml │ │ │ │ └── styles.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ ├── values-night-v31/ │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ └── values-v31/ │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle.kts ├── assets/ │ ├── linux/ │ │ ├── DEBIAN/ │ │ │ ├── control │ │ │ ├── postinst │ │ │ ├── postrm │ │ │ └── prerm │ │ └── com.example.piliplus.desktop │ └── shaders/ │ ├── Anime4K_AutoDownscalePre_x2.glsl │ ├── Anime4K_AutoDownscalePre_x4.glsl │ ├── Anime4K_Clamp_Highlights.glsl │ ├── Anime4K_Restore_CNN_M.glsl │ ├── Anime4K_Restore_CNN_S.glsl │ ├── Anime4K_Restore_CNN_VL.glsl │ ├── Anime4K_Upscale_CNN_x2_M.glsl │ ├── Anime4K_Upscale_CNN_x2_S.glsl │ ├── Anime4K_Upscale_CNN_x2_VL.glsl │ └── LICENSE ├── distribute_options.yaml ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── LaunchBackground.imageset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── 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 ├── lib/ │ ├── build_config.dart │ ├── common/ │ │ ├── constants.dart │ │ ├── skeleton/ │ │ │ ├── dynamic_card.dart │ │ │ ├── fav_pgc_item.dart │ │ │ ├── media_bangumi.dart │ │ │ ├── msg_feed_sys_msg_.dart │ │ │ ├── msg_feed_top.dart │ │ │ ├── skeleton.dart │ │ │ ├── space_opus.dart │ │ │ ├── video_card_h.dart │ │ │ ├── video_card_v.dart │ │ │ ├── video_reply.dart │ │ │ └── whisper_item.dart │ │ └── widgets/ │ │ ├── appbar/ │ │ │ └── appbar.dart │ │ ├── avatars.dart │ │ ├── back_detector.dart │ │ ├── badge.dart │ │ ├── button/ │ │ │ ├── icon_button.dart │ │ │ ├── more_btn.dart │ │ │ └── toolbar_icon_button.dart │ │ ├── color_palette.dart │ │ ├── colored_box_transition.dart │ │ ├── cropped_image.dart │ │ ├── custom_arc.dart │ │ ├── custom_height_widget.dart │ │ ├── custom_icon.dart │ │ ├── custom_toast.dart │ │ ├── custom_tooltip.dart │ │ ├── dialog/ │ │ │ ├── dialog.dart │ │ │ ├── export_import.dart │ │ │ ├── report.dart │ │ │ └── report_member.dart │ │ ├── disabled_icon.dart │ │ ├── dynamic_sliver_app_bar/ │ │ │ ├── dynamic_sliver_app_bar.dart │ │ │ ├── rendering/ │ │ │ │ └── sliver_persistent_header.dart │ │ │ └── sliver_persistent_header.dart │ │ ├── flutter/ │ │ │ ├── chat_list_view.dart │ │ │ ├── draggable_sheet/ │ │ │ │ ├── draggable_scrollable_sheet_dyn.dart │ │ │ │ └── draggable_scrollable_sheet_topic.dart │ │ │ ├── layout_builder.dart │ │ │ ├── list_tile.dart │ │ │ ├── page/ │ │ │ │ ├── page_view.dart │ │ │ │ ├── scrollable.dart │ │ │ │ ├── scrollable_helpers.dart │ │ │ │ └── tabs.dart │ │ │ ├── pop_scope.dart │ │ │ ├── popup_menu.dart │ │ │ ├── refresh_indicator.dart │ │ │ ├── selectable_text/ │ │ │ │ ├── selectable_region.dart │ │ │ │ ├── selectable_text.dart │ │ │ │ ├── selection_area.dart │ │ │ │ ├── tap_and_drag.dart │ │ │ │ ├── text.dart │ │ │ │ └── text_selection.dart │ │ │ ├── sliver_layout_builder.dart │ │ │ ├── tabs.dart │ │ │ ├── text/ │ │ │ │ ├── paragraph.dart │ │ │ │ ├── rich_text.dart │ │ │ │ └── text.dart │ │ │ ├── text_field/ │ │ │ │ ├── adaptive_text_selection_toolbar.dart │ │ │ │ ├── controller.dart │ │ │ │ ├── cupertino/ │ │ │ │ │ ├── adaptive_text_selection_toolbar.dart │ │ │ │ │ ├── spell_check_suggestions_toolbar.dart │ │ │ │ │ └── text_field.dart │ │ │ │ ├── editable.dart │ │ │ │ ├── editable_text.dart │ │ │ │ ├── spell_check.dart │ │ │ │ ├── spell_check_suggestions_toolbar.dart │ │ │ │ ├── system_context_menu.dart │ │ │ │ ├── text_field.dart │ │ │ │ └── text_selection.dart │ │ │ └── vertical_tabs.dart │ │ ├── gesture/ │ │ │ ├── horizontal_drag_gesture_recognizer.dart │ │ │ ├── image_horizontal_drag_gesture_recognizer.dart │ │ │ ├── immediate_tap_gesture_recognizer.dart │ │ │ ├── mouse_interactive_viewer.dart │ │ │ └── tap_gesture_recognizer.dart │ │ ├── image/ │ │ │ ├── cached_network_svg_image.dart │ │ │ ├── image_save.dart │ │ │ └── network_img_layer.dart │ │ ├── image_grid/ │ │ │ ├── image_grid_builder.dart │ │ │ └── image_grid_view.dart │ │ ├── image_viewer/ │ │ │ ├── gallery_viewer.dart │ │ │ ├── hero.dart │ │ │ ├── hero_dialog_route.dart │ │ │ ├── image.dart │ │ │ ├── loading_indicator.dart │ │ │ └── viewer.dart │ │ ├── keep_alive_wrapper.dart │ │ ├── loading_widget/ │ │ │ ├── http_error.dart │ │ │ ├── loading_widget.dart │ │ │ └── m3e_loading_indicator.dart │ │ ├── loading_widget.dart │ │ ├── marquee.dart │ │ ├── only_layout_widget.dart │ │ ├── pair.dart │ │ ├── pendant_avatar.dart │ │ ├── player_bar.dart │ │ ├── progress_bar/ │ │ │ ├── audio_video_progress_bar.dart │ │ │ ├── segment_progress_bar.dart │ │ │ └── video_progress_indicator.dart │ │ ├── radio_widget.dart │ │ ├── route_aware_mixin.dart │ │ ├── scale_app.dart │ │ ├── scroll_behavior.dart │ │ ├── scroll_physics.dart │ │ ├── select_mask.dart │ │ ├── self_sized_horizontal_list.dart │ │ ├── sliver/ │ │ │ ├── sliver_floating_header.dart │ │ │ ├── sliver_pinned_dynamic_header.dart │ │ │ └── sliver_pinned_header.dart │ │ ├── sliver_wrap.dart │ │ ├── stat/ │ │ │ └── stat.dart │ │ ├── stateful_builder.dart │ │ ├── time_picker.dart │ │ ├── video_card/ │ │ │ ├── video_card_h.dart │ │ │ └── video_card_v.dart │ │ ├── video_popup_menu.dart │ │ ├── view_safe_area.dart │ │ └── view_sliver_safe_area.dart │ ├── grpc/ │ │ ├── audio.dart │ │ ├── bilibili/ │ │ │ ├── account/ │ │ │ │ └── service/ │ │ │ │ ├── v1.pb.dart │ │ │ │ ├── v1.pbenum.dart │ │ │ │ └── v1.pbjson.dart │ │ │ ├── app/ │ │ │ │ ├── archive/ │ │ │ │ │ ├── middleware/ │ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ ├── card/ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ ├── dynamic/ │ │ │ │ │ ├── common.pb.dart │ │ │ │ │ ├── common.pbenum.dart │ │ │ │ │ ├── common.pbjson.dart │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ ├── v1.pbjson.dart │ │ │ │ │ ├── v2.pb.dart │ │ │ │ │ ├── v2.pbenum.dart │ │ │ │ │ └── v2.pbjson.dart │ │ │ │ ├── im/ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ ├── listener/ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ ├── playurl/ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ └── viewunite/ │ │ │ │ ├── common.pb.dart │ │ │ │ ├── common.pbenum.dart │ │ │ │ ├── common.pbjson.dart │ │ │ │ ├── pgcanymodel.pb.dart │ │ │ │ ├── pgcanymodel.pbenum.dart │ │ │ │ ├── pgcanymodel.pbjson.dart │ │ │ │ ├── v1.pb.dart │ │ │ │ ├── v1.pbenum.dart │ │ │ │ └── v1.pbjson.dart │ │ │ ├── community/ │ │ │ │ └── service/ │ │ │ │ └── dm/ │ │ │ │ ├── v1.pb.dart │ │ │ │ ├── v1.pbenum.dart │ │ │ │ └── v1.pbjson.dart │ │ │ ├── dagw/ │ │ │ │ └── component/ │ │ │ │ └── avatar/ │ │ │ │ ├── common.pb.dart │ │ │ │ ├── common.pbenum.dart │ │ │ │ ├── common.pbjson.dart │ │ │ │ ├── v1/ │ │ │ │ │ ├── plugin.pb.dart │ │ │ │ │ ├── plugin.pbenum.dart │ │ │ │ │ └── plugin.pbjson.dart │ │ │ │ ├── v1.pb.dart │ │ │ │ ├── v1.pbenum.dart │ │ │ │ └── v1.pbjson.dart │ │ │ ├── im/ │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── v1.pb.dart │ │ │ │ │ ├── v1.pbenum.dart │ │ │ │ │ └── v1.pbjson.dart │ │ │ │ ├── type.pb.dart │ │ │ │ ├── type.pbenum.dart │ │ │ │ └── type.pbjson.dart │ │ │ ├── main/ │ │ │ │ └── community/ │ │ │ │ └── reply/ │ │ │ │ ├── v1.pb.dart │ │ │ │ ├── v1.pbenum.dart │ │ │ │ └── v1.pbjson.dart │ │ │ ├── metadata/ │ │ │ │ ├── device.pb.dart │ │ │ │ ├── device.pbenum.dart │ │ │ │ ├── device.pbjson.dart │ │ │ │ ├── fawkes.pb.dart │ │ │ │ ├── fawkes.pbenum.dart │ │ │ │ ├── fawkes.pbjson.dart │ │ │ │ ├── locale.pb.dart │ │ │ │ ├── locale.pbenum.dart │ │ │ │ ├── locale.pbjson.dart │ │ │ │ ├── network.pb.dart │ │ │ │ ├── network.pbenum.dart │ │ │ │ ├── network.pbjson.dart │ │ │ │ ├── parabox.pb.dart │ │ │ │ ├── parabox.pbenum.dart │ │ │ │ ├── parabox.pbjson.dart │ │ │ │ ├── restriction.pb.dart │ │ │ │ ├── restriction.pbenum.dart │ │ │ │ └── restriction.pbjson.dart │ │ │ ├── metadata.pb.dart │ │ │ ├── metadata.pbenum.dart │ │ │ ├── metadata.pbjson.dart │ │ │ ├── pagination.pb.dart │ │ │ ├── pagination.pbenum.dart │ │ │ ├── pagination.pbjson.dart │ │ │ ├── playershared.pb.dart │ │ │ ├── playershared.pbenum.dart │ │ │ ├── playershared.pbjson.dart │ │ │ ├── rpc.pb.dart │ │ │ ├── rpc.pbenum.dart │ │ │ ├── rpc.pbjson.dart │ │ │ └── vas/ │ │ │ └── garb/ │ │ │ ├── model.pb.dart │ │ │ ├── model.pbenum.dart │ │ │ ├── model.pbjson.dart │ │ │ ├── service.pb.dart │ │ │ ├── service.pbenum.dart │ │ │ └── service.pbjson.dart │ │ ├── dm.dart │ │ ├── dyn.dart │ │ ├── grpc_req.dart │ │ ├── im.dart │ │ ├── reply.dart │ │ ├── space.dart │ │ ├── url.dart │ │ └── view.dart │ ├── http/ │ │ ├── api.dart │ │ ├── black.dart │ │ ├── browser_ua.dart │ │ ├── constants.dart │ │ ├── danmaku.dart │ │ ├── danmaku_block.dart │ │ ├── download.dart │ │ ├── dynamics.dart │ │ ├── fan.dart │ │ ├── fav.dart │ │ ├── follow.dart │ │ ├── init.dart │ │ ├── live.dart │ │ ├── loading_state.dart │ │ ├── login.dart │ │ ├── match.dart │ │ ├── member.dart │ │ ├── msg.dart │ │ ├── music.dart │ │ ├── pgc.dart │ │ ├── reply.dart │ │ ├── retry_interceptor.dart │ │ ├── search.dart │ │ ├── sponsor_block.dart │ │ ├── sponsor_block_api.dart │ │ ├── user.dart │ │ ├── validate.dart │ │ └── video.dart │ ├── main.dart │ ├── models/ │ │ ├── common/ │ │ │ ├── account_type.dart │ │ │ ├── audio_normalization.dart │ │ │ ├── avatar_badge_type.dart │ │ │ ├── badge_type.dart │ │ │ ├── bar_hide_type.dart │ │ │ ├── dm_block_type.dart │ │ │ ├── dynamic/ │ │ │ │ ├── dynamic_badge_mode.dart │ │ │ │ ├── dynamics_type.dart │ │ │ │ └── up_panel_position.dart │ │ │ ├── enum_with_label.dart │ │ │ ├── episode_panel_type.dart │ │ │ ├── fav_order_type.dart │ │ │ ├── fav_type.dart │ │ │ ├── follow_order_type.dart │ │ │ ├── home_tab_type.dart │ │ │ ├── image_preview_type.dart │ │ │ ├── image_type.dart │ │ │ ├── later_view_type.dart │ │ │ ├── live/ │ │ │ │ ├── live_contribution_rank_type.dart │ │ │ │ ├── live_dm_silent_type.dart │ │ │ │ └── live_search_type.dart │ │ │ ├── member/ │ │ │ │ ├── contribute_type.dart │ │ │ │ ├── profile_type.dart │ │ │ │ ├── search_type.dart │ │ │ │ ├── tab_type.dart │ │ │ │ └── user_info_type.dart │ │ │ ├── msg/ │ │ │ │ ├── msg_type.dart │ │ │ │ └── msg_unread_type.dart │ │ │ ├── nav_bar_config.dart │ │ │ ├── pgc_review_type.dart │ │ │ ├── publish_panel_type.dart │ │ │ ├── rank_type.dart │ │ │ ├── reply/ │ │ │ │ ├── reply_option_type.dart │ │ │ │ ├── reply_search_type.dart │ │ │ │ ├── reply_sort_type.dart │ │ │ │ └── reply_type.dart │ │ │ ├── search/ │ │ │ │ ├── article_search_type.dart │ │ │ │ ├── search_type.dart │ │ │ │ ├── user_search_type.dart │ │ │ │ └── video_search_type.dart │ │ │ ├── setting_type.dart │ │ │ ├── sponsor_block/ │ │ │ │ ├── action_type.dart │ │ │ │ ├── post_segment_model.dart │ │ │ │ ├── segment_model.dart │ │ │ │ ├── segment_type.dart │ │ │ │ └── skip_type.dart │ │ │ ├── stat_type.dart │ │ │ ├── super_chat_type.dart │ │ │ ├── super_resolution_type.dart │ │ │ ├── theme/ │ │ │ │ ├── theme_color_type.dart │ │ │ │ └── theme_type.dart │ │ │ ├── video/ │ │ │ │ ├── audio_quality.dart │ │ │ │ ├── cdn_type.dart │ │ │ │ ├── live_quality.dart │ │ │ │ ├── source_type.dart │ │ │ │ ├── subtitle_pref_type.dart │ │ │ │ ├── video_decode_type.dart │ │ │ │ ├── video_quality.dart │ │ │ │ └── video_type.dart │ │ │ └── webview_menu_type.dart │ │ ├── dynamics/ │ │ │ ├── article_content_model.dart │ │ │ ├── result.dart │ │ │ ├── up.dart │ │ │ └── vote_model.dart │ │ ├── home/ │ │ │ └── rcmd/ │ │ │ └── result.dart │ │ ├── login/ │ │ │ └── model.dart │ │ ├── member/ │ │ │ ├── info.dart │ │ │ └── tags.dart │ │ ├── model_avatar.dart │ │ ├── model_hot_video_item.dart │ │ ├── model_owner.dart │ │ ├── model_owner.g.dart │ │ ├── model_rec_video_item.dart │ │ ├── model_video.dart │ │ ├── pgc_lcf.dart │ │ ├── search/ │ │ │ ├── result.dart │ │ │ └── suggest.dart │ │ ├── user/ │ │ │ ├── danmaku_block.dart │ │ │ ├── danmaku_rule.dart │ │ │ ├── danmaku_rule_adapter.dart │ │ │ ├── info.dart │ │ │ ├── info.g.dart │ │ │ ├── stat.dart │ │ │ └── stat.g.dart │ │ └── video/ │ │ └── play/ │ │ └── url.dart │ ├── models_new/ │ │ ├── account_myinfo/ │ │ │ └── data.dart │ │ ├── article/ │ │ │ ├── article_info/ │ │ │ │ ├── data.dart │ │ │ │ ├── share_channel.dart │ │ │ │ └── stats.dart │ │ │ ├── article_list/ │ │ │ │ ├── article.dart │ │ │ │ ├── category.dart │ │ │ │ ├── data.dart │ │ │ │ ├── label.dart │ │ │ │ ├── last.dart │ │ │ │ ├── list.dart │ │ │ │ └── stats.dart │ │ │ └── article_view/ │ │ │ ├── category.dart │ │ │ ├── data.dart │ │ │ ├── label.dart │ │ │ ├── media.dart │ │ │ ├── ops.dart │ │ │ ├── opus.dart │ │ │ ├── stats.dart │ │ │ └── tag.dart │ │ ├── blacklist/ │ │ │ ├── data.dart │ │ │ └── list.dart │ │ ├── coin_log/ │ │ │ ├── data.dart │ │ │ └── list.dart │ │ ├── danmaku/ │ │ │ └── post.dart │ │ ├── download/ │ │ │ ├── bili_download_entry_info.dart │ │ │ ├── bili_download_media_file_info.dart │ │ │ └── download_info.dart │ │ ├── dynamic/ │ │ │ ├── dyn_mention/ │ │ │ │ ├── data.dart │ │ │ │ ├── group.dart │ │ │ │ └── item.dart │ │ │ ├── dyn_reserve/ │ │ │ │ └── data.dart │ │ │ ├── dyn_reserve_info/ │ │ │ │ └── data.dart │ │ │ ├── dyn_topic_feed/ │ │ │ │ ├── all_sort_by.dart │ │ │ │ ├── item.dart │ │ │ │ ├── topic_card_list.dart │ │ │ │ └── topic_sort_by_conf.dart │ │ │ ├── dyn_topic_pub_search/ │ │ │ │ ├── data.dart │ │ │ │ ├── new_topic.dart │ │ │ │ └── page_info.dart │ │ │ └── dyn_topic_top/ │ │ │ ├── top_details.dart │ │ │ ├── topic_creator.dart │ │ │ └── topic_item.dart │ │ ├── emote/ │ │ │ ├── data.dart │ │ │ ├── emote.dart │ │ │ ├── meta.dart │ │ │ └── package.dart │ │ ├── fav/ │ │ │ ├── fav_article/ │ │ │ │ ├── author.dart │ │ │ │ ├── cover.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── stat.dart │ │ │ ├── fav_detail/ │ │ │ │ ├── cnt_info.dart │ │ │ │ ├── data.dart │ │ │ │ ├── info.dart │ │ │ │ ├── media.dart │ │ │ │ ├── ogv.dart │ │ │ │ └── ugc.dart │ │ │ ├── fav_folder/ │ │ │ │ ├── data.dart │ │ │ │ └── list.dart │ │ │ ├── fav_note/ │ │ │ │ ├── arc.dart │ │ │ │ ├── data.dart │ │ │ │ ├── list.dart │ │ │ │ └── page.dart │ │ │ ├── fav_pgc/ │ │ │ │ ├── area.dart │ │ │ │ ├── badge_info.dart │ │ │ │ ├── badge_infos.dart │ │ │ │ ├── cc_on_lock.dart │ │ │ │ ├── config_attrs.dart │ │ │ │ ├── content_attr.dart │ │ │ │ ├── data.dart │ │ │ │ ├── first_ep_info.dart │ │ │ │ ├── highlight_ineffective_hd.dart │ │ │ │ ├── highlight_ineffective_ott.dart │ │ │ │ ├── highlight_ineffective_pink.dart │ │ │ │ ├── list.dart │ │ │ │ ├── multi_img.dart │ │ │ │ ├── new_ep.dart │ │ │ │ ├── producer.dart │ │ │ │ ├── publish.dart │ │ │ │ ├── rating.dart │ │ │ │ ├── rights.dart │ │ │ │ ├── section.dart │ │ │ │ ├── series.dart │ │ │ │ ├── stat.dart │ │ │ │ └── vip_or_pay.dart │ │ │ └── fav_topic/ │ │ │ ├── data.dart │ │ │ ├── page_info.dart │ │ │ ├── topic_item.dart │ │ │ └── topic_list.dart │ │ ├── follow/ │ │ │ ├── data.dart │ │ │ └── list.dart │ │ ├── followee_votes/ │ │ │ └── vote.dart │ │ ├── history/ │ │ │ ├── cursor.dart │ │ │ ├── data.dart │ │ │ ├── history.dart │ │ │ ├── list.dart │ │ │ └── tab.dart │ │ ├── later/ │ │ │ ├── bangumi.dart │ │ │ ├── data.dart │ │ │ ├── list.dart │ │ │ ├── page.dart │ │ │ ├── rights.dart │ │ │ ├── season.dart │ │ │ └── stat.dart │ │ ├── live/ │ │ │ ├── live_area_list/ │ │ │ │ ├── area_item.dart │ │ │ │ └── area_list.dart │ │ │ ├── live_contribution_rank/ │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── medal_info.dart │ │ │ ├── live_danmaku/ │ │ │ │ ├── danmaku_msg.dart │ │ │ │ └── live_emote.dart │ │ │ ├── live_dm_block/ │ │ │ │ ├── data.dart │ │ │ │ ├── shield_info.dart │ │ │ │ ├── shield_rules.dart │ │ │ │ └── shield_user_list.dart │ │ │ ├── live_dm_info/ │ │ │ │ ├── data.dart │ │ │ │ └── host_list.dart │ │ │ ├── live_emote/ │ │ │ │ ├── data.dart │ │ │ │ ├── datum.dart │ │ │ │ └── emoticon.dart │ │ │ ├── live_feed_index/ │ │ │ │ ├── card_data.dart │ │ │ │ ├── card_data_item.dart │ │ │ │ ├── card_data_list_item.dart │ │ │ │ ├── card_list.dart │ │ │ │ ├── data.dart │ │ │ │ └── watched_show.dart │ │ │ ├── live_follow/ │ │ │ │ ├── data.dart │ │ │ │ └── item.dart │ │ │ ├── live_room_info_h5/ │ │ │ │ ├── anchor_info.dart │ │ │ │ ├── base_info.dart │ │ │ │ ├── data.dart │ │ │ │ └── room_info.dart │ │ │ ├── live_room_play_info/ │ │ │ │ ├── codec.dart │ │ │ │ ├── data.dart │ │ │ │ ├── format.dart │ │ │ │ ├── playurl.dart │ │ │ │ ├── playurl_info.dart │ │ │ │ ├── stream.dart │ │ │ │ └── url_info.dart │ │ │ ├── live_search/ │ │ │ │ ├── data.dart │ │ │ │ ├── live_search.dart │ │ │ │ ├── room.dart │ │ │ │ ├── room_item.dart │ │ │ │ ├── user.dart │ │ │ │ └── user_item.dart │ │ │ ├── live_second_list/ │ │ │ │ ├── data.dart │ │ │ │ └── tag.dart │ │ │ └── live_superchat/ │ │ │ ├── data.dart │ │ │ ├── item.dart │ │ │ └── user_info.dart │ │ ├── login_devices/ │ │ │ ├── data.dart │ │ │ └── device.dart │ │ ├── login_log/ │ │ │ ├── data.dart │ │ │ └── list.dart │ │ ├── match/ │ │ │ └── match_info/ │ │ │ ├── contest.dart │ │ │ ├── data.dart │ │ │ ├── home_away.dart │ │ │ ├── season.dart │ │ │ ├── success_teaminfo.dart │ │ │ └── team.dart │ │ ├── media_list/ │ │ │ ├── badge.dart │ │ │ ├── coin.dart │ │ │ ├── data.dart │ │ │ ├── media_list.dart │ │ │ ├── ogv_info.dart │ │ │ ├── page.dart │ │ │ └── rights.dart │ │ ├── member/ │ │ │ ├── coin_like_arc/ │ │ │ │ ├── data.dart │ │ │ │ └── item.dart │ │ │ └── search_archive/ │ │ │ ├── data.dart │ │ │ ├── episodic_button.dart │ │ │ ├── list.dart │ │ │ ├── page.dart │ │ │ └── vlist.dart │ │ ├── member_card_info/ │ │ │ ├── card.dart │ │ │ └── data.dart │ │ ├── msg/ │ │ │ ├── im_user_infos/ │ │ │ │ └── datum.dart │ │ │ ├── msg_at/ │ │ │ │ ├── content.dart │ │ │ │ ├── cursor.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── user.dart │ │ │ ├── msg_dnd/ │ │ │ │ └── uid_setting.dart │ │ │ ├── msg_like/ │ │ │ │ ├── content.dart │ │ │ │ ├── cursor.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ ├── latest.dart │ │ │ │ ├── total.dart │ │ │ │ └── user.dart │ │ │ ├── msg_like_detail/ │ │ │ │ ├── card.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ ├── page.dart │ │ │ │ └── user.dart │ │ │ ├── msg_reply/ │ │ │ │ ├── content.dart │ │ │ │ ├── cursor.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── user.dart │ │ │ ├── msg_sys/ │ │ │ │ ├── data.dart │ │ │ │ ├── publisher.dart │ │ │ │ └── source.dart │ │ │ ├── msgfeed_unread.dart │ │ │ └── session_ss/ │ │ │ └── data.dart │ │ ├── msgfeed_unread/ │ │ │ └── data.dart │ │ ├── music/ │ │ │ ├── bgm_detail.dart │ │ │ └── bgm_recommend_list.dart │ │ ├── pgc/ │ │ │ ├── pgc_index_condition/ │ │ │ │ ├── data.dart │ │ │ │ ├── sort.dart │ │ │ │ └── value.dart │ │ │ ├── pgc_index_result/ │ │ │ │ ├── badge_info.dart │ │ │ │ ├── data.dart │ │ │ │ ├── first_ep.dart │ │ │ │ └── list.dart │ │ │ ├── pgc_info_model/ │ │ │ │ ├── activity.dart │ │ │ │ ├── area.dart │ │ │ │ ├── badge_info.dart │ │ │ │ ├── brief.dart │ │ │ │ ├── cooperator.dart │ │ │ │ ├── danmaku.dart │ │ │ │ ├── ed.dart │ │ │ │ ├── episode.dart │ │ │ │ ├── icon_font.dart │ │ │ │ ├── new_ep.dart │ │ │ │ ├── op.dart │ │ │ │ ├── publish.dart │ │ │ │ ├── rating.dart │ │ │ │ ├── result.dart │ │ │ │ ├── rights.dart │ │ │ │ ├── season.dart │ │ │ │ ├── section.dart │ │ │ │ ├── series.dart │ │ │ │ ├── skip.dart │ │ │ │ ├── stat.dart │ │ │ │ ├── stat_for_unity.dart │ │ │ │ ├── up_info.dart │ │ │ │ ├── user_progress.dart │ │ │ │ ├── user_status.dart │ │ │ │ └── vt.dart │ │ │ ├── pgc_rank/ │ │ │ │ ├── badge_info.dart │ │ │ │ ├── data.dart │ │ │ │ ├── icon_font.dart │ │ │ │ ├── new_ep.dart │ │ │ │ ├── pgc_rank_item_model.dart │ │ │ │ └── stat.dart │ │ │ ├── pgc_review/ │ │ │ │ ├── author.dart │ │ │ │ ├── data.dart │ │ │ │ ├── list.dart │ │ │ │ └── stat.dart │ │ │ └── pgc_timeline/ │ │ │ ├── episode.dart │ │ │ ├── icon_font.dart │ │ │ ├── pgc_timeline.dart │ │ │ └── result.dart │ │ ├── popular/ │ │ │ ├── popular_precious/ │ │ │ │ └── data.dart │ │ │ ├── popular_series_list/ │ │ │ │ └── list.dart │ │ │ └── popular_series_one/ │ │ │ ├── config.dart │ │ │ └── data.dart │ │ ├── reply/ │ │ │ ├── content.dart │ │ │ ├── control.dart │ │ │ ├── cursor.dart │ │ │ ├── data.dart │ │ │ ├── folder.dart │ │ │ ├── level_info.dart │ │ │ ├── member.dart │ │ │ ├── nameplate.dart │ │ │ ├── pagination_reply.dart │ │ │ ├── picture.dart │ │ │ ├── reply.dart │ │ │ ├── reply_control.dart │ │ │ ├── senior.dart │ │ │ ├── top.dart │ │ │ ├── top_reply.dart │ │ │ ├── up_action.dart │ │ │ ├── up_selection.dart │ │ │ └── upper.dart │ │ ├── reply2reply/ │ │ │ ├── data.dart │ │ │ ├── page.dart │ │ │ └── root.dart │ │ ├── reply_interaction/ │ │ │ ├── data.dart │ │ │ └── interact_status.dart │ │ ├── search/ │ │ │ ├── search_rcmd/ │ │ │ │ └── data.dart │ │ │ └── search_trending/ │ │ │ ├── data.dart │ │ │ └── list.dart │ │ ├── single_unread/ │ │ │ └── data.dart │ │ ├── space/ │ │ │ ├── space/ │ │ │ │ ├── achieve.dart │ │ │ │ ├── archive.dart │ │ │ │ ├── article.dart │ │ │ │ ├── attention_tip.dart │ │ │ │ ├── audios.dart │ │ │ │ ├── author.dart │ │ │ │ ├── badge.dart │ │ │ │ ├── button.dart │ │ │ │ ├── card.dart │ │ │ │ ├── category.dart │ │ │ │ ├── cheese.dart │ │ │ │ ├── coin_archive.dart │ │ │ │ ├── collection_top_simple.dart │ │ │ │ ├── color_config.dart │ │ │ │ ├── colour.dart │ │ │ │ ├── comic.dart │ │ │ │ ├── container_size.dart │ │ │ │ ├── cover.dart │ │ │ │ ├── data.dart │ │ │ │ ├── day.dart │ │ │ │ ├── digital_info.dart │ │ │ │ ├── display.dart │ │ │ │ ├── draw.dart │ │ │ │ ├── draw_src.dart │ │ │ │ ├── elec.dart │ │ │ │ ├── elec_list.dart │ │ │ │ ├── elec_set.dart │ │ │ │ ├── entrance.dart │ │ │ │ ├── entrance_button.dart │ │ │ │ ├── entry.dart │ │ │ │ ├── episodic_button.dart │ │ │ │ ├── extra.dart │ │ │ │ ├── favourite2.dart │ │ │ │ ├── followings_followed_upper.dart │ │ │ │ ├── general_spec.dart │ │ │ │ ├── guard.dart │ │ │ │ ├── honours.dart │ │ │ │ ├── images.dart │ │ │ │ ├── item.dart │ │ │ │ ├── label.dart │ │ │ │ ├── level_info.dart │ │ │ │ ├── like_archive.dart │ │ │ │ ├── likes.dart │ │ │ │ ├── list.dart │ │ │ │ ├── live.dart │ │ │ │ ├── live_fans_wearing.dart │ │ │ │ ├── media.dart │ │ │ │ ├── nameplate.dart │ │ │ │ ├── nft.dart │ │ │ │ ├── nft_certificate.dart │ │ │ │ ├── nft_show_module.dart │ │ │ │ ├── night.dart │ │ │ │ ├── official_verify.dart │ │ │ │ ├── order.dart │ │ │ │ ├── play_game.dart │ │ │ │ ├── pos_spec.dart │ │ │ │ ├── pr_info.dart │ │ │ │ ├── preference.dart │ │ │ │ ├── profession_verify.dart │ │ │ │ ├── purchase_button.dart │ │ │ │ ├── relation.dart │ │ │ │ ├── render_spec.dart │ │ │ │ ├── res_native_draw.dart │ │ │ │ ├── resource.dart │ │ │ │ ├── season.dart │ │ │ │ ├── senior_inquiry.dart │ │ │ │ ├── series.dart │ │ │ │ ├── setting.dart │ │ │ │ ├── size_spec.dart │ │ │ │ ├── space_button_list.dart │ │ │ │ ├── space_tag.dart │ │ │ │ ├── stats.dart │ │ │ │ ├── tab.dart │ │ │ │ ├── tab2.dart │ │ │ │ ├── top.dart │ │ │ │ └── ugc_season.dart │ │ │ ├── space_archive/ │ │ │ │ ├── badge.dart │ │ │ │ ├── cursor_attr.dart │ │ │ │ ├── data.dart │ │ │ │ ├── episodic_button.dart │ │ │ │ ├── history.dart │ │ │ │ ├── item.dart │ │ │ │ ├── last_watched_locator.dart │ │ │ │ ├── order.dart │ │ │ │ ├── season.dart │ │ │ │ └── stats.dart │ │ │ ├── space_article/ │ │ │ │ ├── author.dart │ │ │ │ ├── category.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ ├── list.dart │ │ │ │ ├── media.dart │ │ │ │ └── stats.dart │ │ │ ├── space_audio/ │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── statistic.dart │ │ │ ├── space_cheese/ │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── page.dart │ │ │ ├── space_fav/ │ │ │ │ ├── data.dart │ │ │ │ ├── list.dart │ │ │ │ └── media_list_response.dart │ │ │ ├── space_opus/ │ │ │ │ ├── cover.dart │ │ │ │ ├── data.dart │ │ │ │ ├── item.dart │ │ │ │ └── stat.dart │ │ │ ├── space_season_series/ │ │ │ │ ├── archive.dart │ │ │ │ ├── item.dart │ │ │ │ ├── page.dart │ │ │ │ ├── season.dart │ │ │ │ └── stat.dart │ │ │ └── space_shop/ │ │ │ ├── below_label.dart │ │ │ ├── benefit_info.dart │ │ │ ├── cover.dart │ │ │ ├── data.dart │ │ │ ├── item.dart │ │ │ ├── net_price.dart │ │ │ ├── report_params.dart │ │ │ ├── source_desc.dart │ │ │ └── source_front_tag.dart │ │ ├── space_setting/ │ │ │ ├── data.dart │ │ │ └── privacy.dart │ │ ├── sponsor_block/ │ │ │ ├── segment_item.dart │ │ │ └── user_info.dart │ │ ├── sub/ │ │ │ ├── sub/ │ │ │ │ ├── data.dart │ │ │ │ └── list.dart │ │ │ └── sub_detail/ │ │ │ ├── data.dart │ │ │ └── media.dart │ │ ├── triple/ │ │ │ ├── pgc_triple.dart │ │ │ └── ugc_triple.dart │ │ ├── upload_bfs/ │ │ │ └── data.dart │ │ ├── upower_rank/ │ │ │ ├── data.dart │ │ │ ├── level_info.dart │ │ │ ├── rank_info.dart │ │ │ ├── up_info.dart │ │ │ └── user_info.dart │ │ ├── user_real_name/ │ │ │ ├── data.dart │ │ │ └── reject_page.dart │ │ └── video/ │ │ ├── video_ai_conclusion/ │ │ │ ├── data.dart │ │ │ ├── model_result.dart │ │ │ ├── outline.dart │ │ │ ├── part_outline.dart │ │ │ ├── part_subtitle.dart │ │ │ └── subtitle.dart │ │ ├── video_detail/ │ │ │ ├── arc.dart │ │ │ ├── argue_info.dart │ │ │ ├── data.dart │ │ │ ├── desc_v2.dart │ │ │ ├── dimension.dart │ │ │ ├── episode.dart │ │ │ ├── page.dart │ │ │ ├── rights.dart │ │ │ ├── section.dart │ │ │ ├── staff.dart │ │ │ ├── stat.dart │ │ │ ├── stat_detail.dart │ │ │ ├── subtitle.dart │ │ │ ├── ugc_season.dart │ │ │ ├── user_garb.dart │ │ │ └── video_detail_response.dart │ │ ├── video_note_list/ │ │ │ ├── author.dart │ │ │ ├── data.dart │ │ │ ├── list.dart │ │ │ └── page.dart │ │ ├── video_pbp/ │ │ │ └── data.dart │ │ ├── video_play_info/ │ │ │ ├── data.dart │ │ │ ├── interaction.dart │ │ │ ├── subtitle.dart │ │ │ ├── subtitle_info.dart │ │ │ └── view_point.dart │ │ ├── video_relation/ │ │ │ └── data.dart │ │ ├── video_shot/ │ │ │ └── data.dart │ │ ├── video_stein_edgeinfo/ │ │ │ ├── choice.dart │ │ │ ├── data.dart │ │ │ ├── edges.dart │ │ │ ├── preload.dart │ │ │ ├── question.dart │ │ │ ├── skin.dart │ │ │ ├── story_list.dart │ │ │ └── video.dart │ │ └── video_tag/ │ │ └── data.dart │ ├── pages/ │ │ ├── about/ │ │ │ └── view.dart │ │ ├── article/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── article_ops.dart │ │ │ ├── html_render.dart │ │ │ └── opus_content.dart │ │ ├── article_list/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── audio/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── blacklist/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── coin_log/ │ │ │ └── controller.dart │ │ ├── common/ │ │ │ ├── common_controller.dart │ │ │ ├── common_data_controller.dart │ │ │ ├── common_intro_controller.dart │ │ │ ├── common_list_controller.dart │ │ │ ├── common_page.dart │ │ │ ├── common_whisper_controller.dart │ │ │ ├── dyn/ │ │ │ │ ├── common_dyn_controller.dart │ │ │ │ └── common_dyn_page.dart │ │ │ ├── fab_mixin.dart │ │ │ ├── multi_select/ │ │ │ │ ├── base.dart │ │ │ │ └── multi_select_controller.dart │ │ │ ├── publish/ │ │ │ │ ├── common_publish_page.dart │ │ │ │ ├── common_rich_text_pub_page.dart │ │ │ │ ├── common_text_pub_page.dart │ │ │ │ └── publish_route.dart │ │ │ ├── reply_controller.dart │ │ │ ├── search/ │ │ │ │ ├── common_search_controller.dart │ │ │ │ └── common_search_page.dart │ │ │ └── slide/ │ │ │ └── common_slide_page.dart │ │ ├── contact/ │ │ │ └── view.dart │ │ ├── danmaku/ │ │ │ ├── controller.dart │ │ │ ├── danmaku_model.dart │ │ │ └── view.dart │ │ ├── danmaku_block/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── dlna/ │ │ │ └── view.dart │ │ ├── download/ │ │ │ ├── controller.dart │ │ │ ├── detail/ │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── item.dart │ │ │ ├── downloading/ │ │ │ │ └── view.dart │ │ │ ├── search/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ └── view.dart │ │ ├── dynamics/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── action_panel.dart │ │ │ ├── additional_panel.dart │ │ │ ├── author_panel.dart │ │ │ ├── blocked_item.dart │ │ │ ├── content_panel.dart │ │ │ ├── dyn_content.dart │ │ │ ├── dynamic_panel.dart │ │ │ ├── forward_panel.dart │ │ │ ├── interaction.dart │ │ │ ├── live_panel.dart │ │ │ ├── live_panel_sub.dart │ │ │ ├── live_rcmd_panel.dart │ │ │ ├── module_panel.dart │ │ │ ├── rich_node_panel.dart │ │ │ ├── up_panel.dart │ │ │ ├── video_panel.dart │ │ │ └── vote.dart │ │ ├── dynamics_create/ │ │ │ └── view.dart │ │ ├── dynamics_create_reserve/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── dynamics_create_vote/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── dynamics_detail/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── dynamics_mention/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── dynamics_repost/ │ │ │ └── view.dart │ │ ├── dynamics_select_topic/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── dynamics_tab/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── dynamics_topic/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── dynamics_topic_rcmd/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── emote/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── episode_panel/ │ │ │ └── view.dart │ │ ├── exp_log/ │ │ │ └── controller.dart │ │ ├── fan/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── fav/ │ │ │ ├── article/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widget/ │ │ │ │ └── item.dart │ │ │ ├── cheese/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── note/ │ │ │ │ ├── child_view.dart │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widget/ │ │ │ │ └── item.dart │ │ │ ├── pgc/ │ │ │ │ ├── child_view.dart │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widget/ │ │ │ │ └── item.dart │ │ │ ├── topic/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── video/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── item.dart │ │ │ └── view.dart │ │ ├── fav_create/ │ │ │ └── view.dart │ │ ├── fav_detail/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── fav_video_card.dart │ │ ├── fav_folder_sort/ │ │ │ └── view.dart │ │ ├── fav_panel/ │ │ │ └── view.dart │ │ ├── fav_search/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── fav_sort/ │ │ │ └── view.dart │ │ ├── follow/ │ │ │ ├── child/ │ │ │ │ ├── child_controller.dart │ │ │ │ └── child_view.dart │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── follow_item.dart │ │ ├── follow_search/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── follow_type/ │ │ │ ├── controller.dart │ │ │ ├── follow_same/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── followed/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── group_panel/ │ │ │ └── view.dart │ │ ├── history/ │ │ │ ├── base_controller.dart │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── history_search/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── home/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── hot/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── later/ │ │ │ ├── base_controller.dart │ │ │ ├── child_view.dart │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── video_card_h_later.dart │ │ ├── later_search/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── live/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── live_item_app.dart │ │ ├── live_area/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── live_area_detail/ │ │ │ ├── child/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── live_dm_block/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── live_emote/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── live_follow/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── live_item_follow.dart │ │ ├── live_room/ │ │ │ ├── contribution_rank/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── controller.dart │ │ │ ├── send_danmaku/ │ │ │ │ └── view.dart │ │ │ ├── superchat/ │ │ │ │ ├── superchat_card.dart │ │ │ │ └── superchat_panel.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── bottom_control.dart │ │ │ ├── chat_panel.dart │ │ │ └── header_control.dart │ │ ├── live_search/ │ │ │ ├── child/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── live_search_room.dart │ │ │ └── live_search_user.dart │ │ ├── log_table/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── login/ │ │ │ ├── controller.dart │ │ │ ├── geetest/ │ │ │ │ └── geetest_webview_dialog.dart │ │ │ └── view.dart │ │ ├── login_devices/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── login_log/ │ │ │ └── controller.dart │ │ ├── main/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── main_reply/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── match_info/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── member/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ ├── header_layout_widget.dart │ │ │ └── user_info_card.dart │ │ ├── member_article/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── item.dart │ │ ├── member_audio/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── member_cheese/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── member_coin_arc/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── member_comic/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── member_contribute/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── member_dynamics/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── member_favorite/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── item.dart │ │ ├── member_home/ │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── fav_item.dart │ │ │ └── video_card_v_member_home.dart │ │ ├── member_like_arc/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── member_opus/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── space_opus_item.dart │ │ ├── member_pgc/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── pgc_card_v_member_pgc.dart │ │ ├── member_profile/ │ │ │ └── view.dart │ │ ├── member_search/ │ │ │ ├── child/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── member_season_series/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── season_series_card.dart │ │ ├── member_shop/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── member_upower_rank/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── member_video/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── video_card_h_member_video.dart │ │ ├── mine/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── msg_feed_top/ │ │ │ ├── at_me/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── like_detail/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── like_me/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── reply_me/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ └── sys_msg/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── music/ │ │ │ ├── controller.dart │ │ │ ├── video/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── music_video_card_h.dart │ │ ├── my_reply/ │ │ │ └── view.dart │ │ ├── pgc/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── pgc_card_v.dart │ │ │ └── pgc_card_v_timeline.dart │ │ ├── pgc_index/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── pgc_card_v_pgc_index.dart │ │ ├── pgc_review/ │ │ │ ├── child/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── post/ │ │ │ │ └── view.dart │ │ │ └── view.dart │ │ ├── popular_precious/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── popular_series/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── rank/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── zone/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── pgc_rank_item.dart │ │ ├── rcmd/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── save_panel/ │ │ │ └── view.dart │ │ ├── search/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── hot_keyword.dart │ │ │ └── search_text.dart │ │ ├── search_panel/ │ │ │ ├── all/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── pgc_card_v_search.dart │ │ │ ├── article/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── item.dart │ │ │ ├── controller.dart │ │ │ ├── live/ │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── item.dart │ │ │ ├── pgc/ │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── item.dart │ │ │ ├── user/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ └── item.dart │ │ │ ├── video/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ └── view.dart │ │ ├── search_result/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── search_trending/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── setting/ │ │ │ ├── extra_setting.dart │ │ │ ├── models/ │ │ │ │ ├── extra_settings.dart │ │ │ │ ├── model.dart │ │ │ │ ├── play_settings.dart │ │ │ │ ├── privacy_settings.dart │ │ │ │ ├── recommend_settings.dart │ │ │ │ ├── style_settings.dart │ │ │ │ └── video_settings.dart │ │ │ ├── pages/ │ │ │ │ ├── bar_set.dart │ │ │ │ ├── color_select.dart │ │ │ │ ├── display_mode.dart │ │ │ │ ├── font_size_select.dart │ │ │ │ ├── logs.dart │ │ │ │ └── play_speed_set.dart │ │ │ ├── play_setting.dart │ │ │ ├── privacy_setting.dart │ │ │ ├── recommend_setting.dart │ │ │ ├── slide_color_picker.dart │ │ │ ├── style_setting.dart │ │ │ ├── video_setting.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ ├── checkbox_num.dart │ │ │ ├── checkbox_num_list_tile.dart │ │ │ ├── dual_slider_dialog.dart │ │ │ ├── multi_select_dialog.dart │ │ │ ├── normal_item.dart │ │ │ ├── ordered_multi_select_dialog.dart │ │ │ ├── popup_item.dart │ │ │ ├── select_dialog.dart │ │ │ ├── slider_dialog.dart │ │ │ └── switch_item.dart │ │ ├── settings_search/ │ │ │ └── view.dart │ │ ├── share/ │ │ │ └── view.dart │ │ ├── space_setting/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── sponsor_block/ │ │ │ ├── block_mixin.dart │ │ │ └── view.dart │ │ ├── subscription/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── subscription_detail/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── sub_video_card.dart │ │ ├── video/ │ │ │ ├── ai_conclusion/ │ │ │ │ └── view.dart │ │ │ ├── controller.dart │ │ │ ├── download_panel/ │ │ │ │ └── view.dart │ │ │ ├── introduction/ │ │ │ │ ├── local/ │ │ │ │ │ ├── controller.dart │ │ │ │ │ └── view.dart │ │ │ │ ├── pgc/ │ │ │ │ │ ├── controller.dart │ │ │ │ │ ├── view.dart │ │ │ │ │ └── widgets/ │ │ │ │ │ ├── intro_detail.dart │ │ │ │ │ └── pgc_panel.dart │ │ │ │ └── ugc/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ ├── action_item.dart │ │ │ │ ├── menu_row.dart │ │ │ │ ├── page.dart │ │ │ │ ├── season.dart │ │ │ │ └── triple_mixin.dart │ │ │ ├── medialist/ │ │ │ │ └── view.dart │ │ │ ├── member/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── note/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── pay_coins/ │ │ │ │ └── view.dart │ │ │ ├── post_panel/ │ │ │ │ ├── popup_menu_text.dart │ │ │ │ └── view.dart │ │ │ ├── related/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── reply/ │ │ │ │ ├── controller.dart │ │ │ │ ├── view.dart │ │ │ │ └── widgets/ │ │ │ │ ├── reply_item_grpc.dart │ │ │ │ └── zan_grpc.dart │ │ │ ├── reply_new/ │ │ │ │ └── view.dart │ │ │ ├── reply_reply/ │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── reply_search_item/ │ │ │ │ ├── child/ │ │ │ │ │ ├── controller.dart │ │ │ │ │ ├── view.dart │ │ │ │ │ └── widgets/ │ │ │ │ │ └── item.dart │ │ │ │ ├── controller.dart │ │ │ │ └── view.dart │ │ │ ├── send_danmaku/ │ │ │ │ └── view.dart │ │ │ ├── view.dart │ │ │ ├── view_point/ │ │ │ │ └── view.dart │ │ │ └── widgets/ │ │ │ ├── header_control.dart │ │ │ ├── header_mixin.dart │ │ │ └── player_focus.dart │ │ ├── webdav/ │ │ │ ├── view.dart │ │ │ └── webdav.dart │ │ ├── webview/ │ │ │ └── view.dart │ │ ├── whisper/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widgets/ │ │ │ └── item.dart │ │ ├── whisper_block/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── whisper_detail/ │ │ │ ├── controller.dart │ │ │ ├── view.dart │ │ │ └── widget/ │ │ │ └── chat_item.dart │ │ ├── whisper_link_setting/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ ├── whisper_secondary/ │ │ │ ├── controller.dart │ │ │ └── view.dart │ │ └── whisper_settings/ │ │ ├── controller.dart │ │ ├── view.dart │ │ └── widgets/ │ │ └── item.dart │ ├── plugin/ │ │ └── pl_player/ │ │ ├── controller.dart │ │ ├── models/ │ │ │ ├── audio_output_type.dart │ │ │ ├── bottom_control_type.dart │ │ │ ├── bottom_progress_behavior.dart │ │ │ ├── data_source.dart │ │ │ ├── data_status.dart │ │ │ ├── double_tap_type.dart │ │ │ ├── duration.dart │ │ │ ├── fullscreen_mode.dart │ │ │ ├── gesture_type.dart │ │ │ ├── heart_beat_type.dart │ │ │ ├── hwdec_type.dart │ │ │ ├── play_repeat.dart │ │ │ ├── play_speed.dart │ │ │ ├── play_status.dart │ │ │ └── video_fit_type.dart │ │ ├── utils/ │ │ │ ├── danmaku_options.dart │ │ │ └── fullscreen.dart │ │ ├── view/ │ │ │ ├── view.dart │ │ │ └── widgets.dart │ │ └── widgets/ │ │ ├── app_bar_ani.dart │ │ ├── backward_seek.dart │ │ ├── bottom_control.dart │ │ ├── common_btn.dart │ │ ├── forward_seek.dart │ │ ├── mpv_convert_webp.dart │ │ └── play_pause_btn.dart │ ├── router/ │ │ └── app_pages.dart │ ├── scripts/ │ │ ├── bottom_sheet.patch │ │ ├── build.ps1 │ │ ├── modal_barrier.patch │ │ ├── mouse_cursor.patch │ │ └── patch.ps1 │ ├── services/ │ │ ├── account_service.dart │ │ ├── audio_handler.dart │ │ ├── audio_session.dart │ │ ├── download/ │ │ │ ├── download_manager.dart │ │ │ └── download_service.dart │ │ ├── logger.dart │ │ ├── service_locator.dart │ │ └── shutdown_timer_service.dart │ ├── tcp/ │ │ └── live.dart │ └── utils/ │ ├── accounts/ │ │ ├── account.dart │ │ ├── account_adapter.dart │ │ ├── account_manager/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── account_mgr.dart │ │ ├── account_type_adapter.dart │ │ ├── api_type.dart │ │ ├── cookie_jar_adapter.dart │ │ └── grpc_headers.dart │ ├── accounts.dart │ ├── app_scheme.dart │ ├── app_sign.dart │ ├── asset_utils.dart │ ├── cache_manager.dart │ ├── calc_window_position.dart │ ├── danmaku_utils.dart │ ├── date_utils.dart │ ├── duration_utils.dart │ ├── em.dart │ ├── extension/ │ │ ├── box_ext.dart │ │ ├── context_ext.dart │ │ ├── extension.dart │ │ ├── file_ext.dart │ │ ├── get_ext.dart │ │ ├── iterable_ext.dart │ │ ├── map_ext.dart │ │ ├── num_ext.dart │ │ ├── scroll_controller_ext.dart │ │ ├── size_ext.dart │ │ ├── string_ext.dart │ │ ├── theme_ext.dart │ │ ├── three_dot_ext.dart │ │ └── widget_ext.dart │ ├── fav_utils.dart │ ├── feed_back.dart │ ├── global_data.dart │ ├── grid.dart │ ├── id_utils.dart │ ├── image_utils.dart │ ├── json_file_handler.dart │ ├── login_utils.dart │ ├── num_utils.dart │ ├── page_utils.dart │ ├── parse_string.dart │ ├── path_utils.dart │ ├── permission_handler.dart │ ├── platform_utils.dart │ ├── recommend_filter.dart │ ├── reply_utils.dart │ ├── request_utils.dart │ ├── set_int_adapter.dart │ ├── storage.dart │ ├── storage_key.dart │ ├── storage_pref.dart │ ├── theme_utils.dart │ ├── update.dart │ ├── url_utils.dart │ ├── utils.dart │ ├── video_utils.dart │ ├── waterfall.dart │ └── wbi_sign.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ └── CMakeLists.txt │ └── runner/ │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ └── Flutter-Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ └── Runner.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── pubspec.yaml └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ └── CMakeLists.txt ├── packaging/ │ └── exe/ │ ├── ChineseSimplified.isl │ ├── inno_setup.iss │ └── make_config.yaml └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .fvmrc ================================================ { "flutter": "3.41.5" } ================================================ FILE: .github/ISSUE_TEMPLATE/bug-反馈.yml ================================================ name: Bug 反馈 description: 描述你所遇到的bug labels: [ "bug" ] title: "[Bug] " body: - type: checkboxes id: checklist attributes: label: 检查清单 options: - label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同问题 required: true - label: 正在使用最新版本。 required: true - label: 已排除网络问题 required: true - label: 已排除账号问题 required: true - label: 已排除设置问题 required: true - type: checkboxes id: assign attributes: label: Assign options: - label: self-assign required: false - type: textarea id: version attributes: label: 版本号 validations: required: true - type: textarea id: steps attributes: label: 复现步骤 description: 请提供复现该问题所需的具体步骤。 validations: required: true - type: textarea id: expected attributes: label: 预期行为 description: 请描述你期望的正确行为或结果。 validations: required: true - type: textarea id: actual attributes: label: 实际行为 description: 请描述实际的行为或结果。 validations: required: true - type: textarea id: log attributes: label: 错误日志 description: 请提供设置->关于->错误日志中的内容,粘贴在下方代码框中。如果没有,请提供您的app版本号、系统版本、设备型号等相关信息。 - type: textarea id: info attributes: label: 相关信息 description: 请补充截图、录屏、BV号等其他有助于解决问题的信息。 ================================================ FILE: .github/ISSUE_TEMPLATE/功能请求.yml ================================================ name: 功能请求 description: 对于功能的一些建议 labels: [ "enhancement" ] title: "[FR] " body: - type: checkboxes id: checklist attributes: label: 检查清单 options: - label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同功能请求 required: true - label: 正在使用最新版本。 required: true - label: 设置中未搜索到该功能 required: true - type: checkboxes id: assign attributes: label: Assign options: - label: self-assign required: false - type: textarea id: desc attributes: label: 功能描述 description: 请提供对所请求功能的清晰描述。 validations: required: true - type: textarea id: solution attributes: label: 解决方案 description: 如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。 - type: textarea id: addition attributes: label: 其他 description: 请提供已实现该功能或类似功能的应用 ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: pull_request: types: - opened - synchronize - reopened - ready_for_review paths-ignore: - "**.md" workflow_dispatch: inputs: build_android: description: "Build Android" required: false default: true type: boolean build_ios: description: "Build iOS" required: false default: true type: boolean build_mac: description: "Build Mac" required: false default: true type: boolean build_win_x64: description: "Build Win-x64" required: false default: true type: boolean build_linux_x64: description: "Build Linux-x64" required: false default: true type: boolean tag: description: "tag" required: false default: "" type: string jobs: android: if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_android == 'true' }} name: Release Android runs-on: ubuntu-latest permissions: write-all steps: - name: 代码迁出 uses: actions/checkout@v6 with: fetch-depth: 0 - name: 构建Java环境 uses: actions/setup-java@v5 with: distribution: "zulu" java-version: "17" cache: "gradle" cache-dependency-path: | android/*.gradle* android/**/gradle-wrapper.properties - name: 安装Flutter uses: subosito/flutter-action@v2 id: flutter-action with: channel: stable flutter-version-file: pubspec.yaml cache: true - name: Apply Patch shell: pwsh run: lib/scripts/patch.ps1 android continue-on-error: true - name: Write key if: github.event_name == 'workflow_dispatch' run: | if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks echo storeFile='key.jks' >> android/key.properties echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties fi - name: Set and Extract version if: ${{ github.event_name == 'workflow_dispatch' }} shell: pwsh run: lib/scripts/build.ps1 android - name: Flutter Build Release Apk if: ${{ github.event_name == 'workflow_dispatch' }} run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub - name: Flutter Build Dev Apk if: ${{ github.event_name == 'pull_request' }} run: | flutter build apk --release --split-per-abi --android-project-arg dev=1 --pub - name: Rename run: | for file in build/app/outputs/flutter-apk/app-*-release.apk; do abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|') mv "$file" "PiliPlus_android_${{ env.version }}_${abi}.apk" done shell: bash - name: Release if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }} uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag }} name: ${{ github.event.inputs.tag }} files: PiliPlus_android_*.apk - name: 上传 uses: actions/upload-artifact@v7 with: archive: false name: Android_arm64-v8a path: PiliPlus_android_*_arm64-v8a.apk - name: 上传 uses: actions/upload-artifact@v7 with: archive: false name: Android_armeabi-v7a path: PiliPlus_android_*_armeabi-v7a.apk - name: 上传 uses: actions/upload-artifact@v7 with: archive: false name: Android_x86_64 path: PiliPlus_android_*_x86_64.apk ios: if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_ios == 'true' }} uses: ./.github/workflows/ios.yml permissions: write-all with: tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} mac: if: ${{ github.event.inputs.build_mac == 'true' }} uses: ./.github/workflows/mac.yml permissions: write-all with: tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} win_x64: if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_win_x64 == 'true' }} uses: ./.github/workflows/win_x64.yml permissions: write-all with: tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} linux_x64: if: ${{ github.event.inputs.build_linux_x64 == 'true' }} uses: ./.github/workflows/linux_x64.yml permissions: write-all with: tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} ================================================ FILE: .github/workflows/ios.yml ================================================ name: Build for iOS on: workflow_call: inputs: tag: description: "tag" required: false default: "" type: string workflow_dispatch: jobs: build-macos-app: name: Release IOS runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version-file: pubspec.yaml - name: Set and Extract version shell: pwsh run: lib/scripts/build.ps1 - name: Apply Patch shell: pwsh run: lib/scripts/patch.ps1 iOS continue-on-error: true - name: Build iOS run: | flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json ln -sf ./build/ios/iphoneos Payload # make AltSign happy... find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \; zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/Runner.app - name: Release if: ${{ github.event.inputs.tag != '' }} uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag }} name: ${{ github.event.inputs.tag }} files: | PiliPlus_ios_*.ipa - name: Upload ios release uses: actions/upload-artifact@v7 with: archive: false name: iOS-release path: PiliPlus_ios_*.ipa ================================================ FILE: .github/workflows/linux_x64.yml ================================================ name: Build for Linux x64 on: workflow_call: inputs: tag: description: "tag" required: false default: "" type: string workflow_dispatch: jobs: build-linux-app: name: Release Linux x64 runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev rpm patchelf sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \ libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \ libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \ libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \ libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \ libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \ libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \ libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \ libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \ libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \ linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \ x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \ unixodbc-dev libpq-dev libxxhash-dev libaom-dev \ libgtk-3-0 libblkid1 liblzma5 libmpv-dev shell: bash - name: Setup flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version-file: pubspec.yaml cache: true - name: Set and Extract version shell: pwsh run: lib/scripts/build.ps1 - name: Apply Patch shell: pwsh run: lib/scripts/patch.ps1 Linux continue-on-error: true #TODO: deb and rpm packages need to be build - name: Build Linux run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json - name: Package .tar.gz run: tar -zcvf PiliPlus_linux_${{ env.version }}_amd64.tar.gz -C build/linux/x64/release/bundle . - name: Packege deb run: | printf "建立构建目录...\n" mkdir "PiliPlus_linux_${{ env.version }}_amd64" pushd "PiliPlus_linux_${{ env.version }}_amd64" mkdir -p opt/PiliPlus mkdir -p usr/share/applications mkdir -p usr/share/icons/hicolor/512x512/apps printf "复制文件...\n" cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus cp -r ../assets/linux/DEBIAN . cp ../assets/linux/com.example.piliplus.desktop usr/share/applications cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png printf "修改控制文件...\n" # 替换版本号 sed -i "2s/version_need_change/${{ env.version }}/g" DEBIAN/control # 计算安装大小并替换 SIZE_KB=$(du -s -b --apparent-size . | awk '{print int($1)}') SIZE_KB=$(($SIZE_KB - $(du -s -b --apparent-size DEBIAN | awk '{print int($1)}'))) SIZE_KB=$(echo $SIZE_KB | awk '{print int($1/1024 + 0.999)}') printf "\t安装大小: %s KB\n" "$SIZE_KB" sed -i "9s/size_need_change/${SIZE_KB}/g" DEBIAN/control printf "生成并写入 md5sums ...\n" md5sum opt/PiliPlus/piliplus >> DEBIAN/md5sums md5sum opt/PiliPlus/lib/* >> DEBIAN/md5sums md5sum opt/PiliPlus/data/icudtl.dat >> DEBIAN/md5sums printf "设置权限...\n" chmod 0644 DEBIAN/control chmod 0644 DEBIAN/md5sums chmod 0755 DEBIAN/postinst chmod 0755 DEBIAN/postrm chmod 0755 DEBIAN/prerm printf "打包 deb 文件...\n" popd dpkg-deb --build --verbose --root-owner-group "PiliPlus_linux_${{ env.version }}_amd64" printf "完成: PiliPlus_linux_%s_amd64.deb\n" "${{ env.version }}" shell: bash - name: Packege rpm run: | printf "建立 RPM 构建目录...\n" RPM_BUILD_ROOT="$PWD/rpm_build" mkdir -p "$RPM_BUILD_ROOT/BUILD" "$RPM_BUILD_ROOT/RPMS" "$RPM_BUILD_ROOT/SOURCES" "$RPM_BUILD_ROOT/SPECS" "$RPM_BUILD_ROOT/SRPMS" printf "准备源码归档(仅包含运行时与元数据)...\n" DATE="$(date '+%a %b %d %Y')" SRC_DIR="$PWD/piliplus-${{ env.version }}" mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets" cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/" cp assets/linux/com.example.piliplus.desktop "$SRC_DIR/assets/com.example.piliplus.desktop" cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png" tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}" printf "生成 spec 文件...\n" cat > "$RPM_BUILD_ROOT/SPECS/piliplus.spec" < "$APPDIR/AppRun" <<'APPRUN_EOF' #!/bin/bash SELF=$(readlink -f "$0") HERE=${SELF%/*} export PATH="${HERE}/usr/bin:${PATH}" export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}" exec "${HERE}/usr/bin/piliplus" "$@" APPRUN_EOF chmod +x "$APPDIR/AppRun" printf "修改桌面文件中的 Exec 路径...\n" sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/com.example.piliplus.desktop" sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/com.example.piliplus.desktop" printf "打包 AppImage...\n" ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage" printf "完成: PiliPlus_linux_%s_amd64.AppImage\n" "${{ env.version }}" shell: bash - name: Release if: ${{ github.event.inputs.tag != '' }} uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag }} name: ${{ github.event.inputs.tag }} files: | PiliPlus_linux_*.tar.gz PiliPlus_linux_*.deb PiliPlus_linux_*.rpm PiliPlus_linux_*.AppImage - name: Upload linux targz package uses: actions/upload-artifact@v7 with: archive: false name: Linux_targz_amd64_packege path: PiliPlus_linux_*.tar.gz - name: Upload linux deb package uses: actions/upload-artifact@v7 with: archive: false name: Linux_deb_amd64_package path: PiliPlus_linux_*.deb - name: Upload linux rpm package uses: actions/upload-artifact@v7 with: archive: false name: Linux_rpm_amd64_package path: PiliPlus_linux_*.rpm - name: Upload linux AppImage package uses: actions/upload-artifact@v7 with: archive: false name: Linux_AppImage_amd64_package path: PiliPlus_linux_*.AppImage ================================================ FILE: .github/workflows/mac.yml ================================================ name: Build for Mac on: workflow_call: inputs: tag: description: "tag" required: false default: "" type: string workflow_dispatch: jobs: build-mac-app: name: Release Mac runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version-file: pubspec.yaml - name: Set and Extract version shell: pwsh run: lib/scripts/build.ps1 - name: Apply Patch shell: pwsh run: lib/scripts/patch.ps1 macOS continue-on-error: true - name: Build Mac run: flutter build macos --release --dart-define-from-file=pili_release.json - name: Prepare Upload run: | npm install --global create-dmg create-dmg build/macos/Build/Products/Release/PiliPlus.app || true continue-on-error: true - name: Rename DMG run: mv PiliPlus*.dmg PiliPlus_macos_${{ env.version }}.dmg - name: Release if: ${{ github.event.inputs.tag != '' }} uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag }} name: ${{ github.event.inputs.tag }} files: | PiliPlus_macos_*.dmg - name: Upload macos release uses: actions/upload-artifact@v7 with: archive: false name: macOS-release path: PiliPlus_macos_*.dmg ================================================ FILE: .github/workflows/win_x64.yml ================================================ name: Build for Windows x64 on: workflow_call: inputs: tag: description: "tag" required: false default: "" type: string workflow_dispatch: jobs: build-windows-app: name: Release Windows x64 runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version-file: pubspec.yaml - name: Apply Patch shell: pwsh run: lib/scripts/patch.ps1 windows continue-on-error: true - name: Add fastforge and Inno Setup run: | dart pub global activate fastforge choco install innosetup - name: Add Chinese language file for Inno Setup run: | Copy-Item "windows/packaging/exe/ChineseSimplified.isl" "C:\Program Files (x86)\Inno Setup 6\Languages\ChineseSimplified.isl" shell: pwsh - name: Set and Extract version shell: pwsh run: lib/scripts/build.ps1 - name: Build Windows run: | fastforge package --platform windows --targets exe --flutter-build-args="dart-define-from-file=pili_release.json" - name: Prepare Upload run: | mkdir -p Release/PiliPlus-Win mkdir -p PiliPlus-Win-Setup mv build/windows/x64/runner/Release/* Release/PiliPlus-Win/ mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe - name: Compress if: ${{ github.event.inputs.tag != '' }} run: | Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64_portable.zip" shell: pwsh - name: Release if: ${{ github.event.inputs.tag != '' }} uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag }} name: ${{ github.event.inputs.tag }} files: | PiliPlus_windows_*.zip PiliPlus-Win-Setup/PiliPlus_windows_*.exe - name: Upload windows file release uses: actions/upload-artifact@v7 with: archive: true name: PiliPlus_windows_${{env.version}}_x64_portable path: Release - name: Upload windows setup release uses: actions/upload-artifact@v7 with: archive: false name: Windows-setup-x64-release path: PiliPlus-Win-Setup/PiliPlus_windows_*.exe ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. .vscode/ # Flutter repo-specific /bin/cache/ /bin/internal/bootstrap.bat /bin/internal/bootstrap.sh /bin/mingit/ /dev/benchmarks/mega_gallery/ /dev/bots/.recipe_deps /dev/bots/android_tools/ /dev/devicelab/ABresults*.json /dev/docs/doc/ /dev/docs/api_docs.zip /dev/docs/flutter.docs.zip /dev/docs/lib/ /dev/docs/pubspec.yaml /dev/integration_tests/**/xcuserdata /dev/integration_tests/**/Pods /packages/flutter/coverage/ version analysis_benchmark.json # packages file containing multi-root paths .packages.generated # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ flutter_*.png linked_*.ds unlinked.ds unlinked_spec.ds # Obfuscation related app.*.map.json # Android related **/android/**/gradle-wrapper.jar .gradle/ **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java **/android/key.properties *.jks # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/.last_build_id **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/ephemeral **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # macOS **/Flutter/ephemeral/ **/Pods/ **/macos/Flutter/GeneratedPluginRegistrant.swift **/macos/Flutter/ephemeral **/xcuserdata/ # Windows **/windows/flutter/generated_plugin_registrant.cc **/windows/flutter/generated_plugin_registrant.h **/windows/flutter/generated_plugins.cmake # Linux **/linux/flutter/generated_plugin_registrant.cc **/linux/flutter/generated_plugin_registrant.h **/linux/flutter/generated_plugins.cmake # Coverage coverage/ # Symbols app.*.symbols # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock !.vscode/settings.json !.vscode/launch.json !.vscode/tasks.json devtools_options.yaml # FVM Version Cache .fvm/ pili_release.json dist test*.dart ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: 4b12645012342076800eb701bcdfe18f87da21cf channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf - platform: android create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf - platform: ios create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf - platform: linux create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf - platform: macos create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf - platform: web create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf - platform: windows create_revision: 4b12645012342076800eb701bcdfe18f87da21cf base_revision: 4b12645012342076800eb701bcdfe18f87da21cf # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: .vscode/launch.json ================================================ { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "PiliPlus", "request": "launch", "type": "dart" }, { "name": "PiliPlus (profile mode)", "request": "launch", "type": "dart", "flutterMode": "profile" }, { "name": "PiliPlus (release mode)", "request": "launch", "type": "dart", "flutterMode": "release" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "[dart]": { "editor.formatOnType": true }, "editor.codeActionsOnSave": { "source.organizeImports": "explicit", // "source.fixAll": "explicit", } } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================

PiliPlus

![GitHub repo size](https://img.shields.io/github/repo-size/bggRGjQaUbCoE/PiliPlus) ![GitHub Repo stars](https://img.shields.io/github/stars/bggRGjQaUbCoE/PiliPlus) ![GitHub all releases](https://img.shields.io/github/downloads/bggRGjQaUbCoE/PiliPlus/total)

使用Flutter开发的BiliBili第三方客户端

home home home
home

## 适配平台 - [x] Android - [x] iOS - [x] Pad - [x] Windows - [x] Linux [![Packaging status](https://repology.org/badge/vertical-allrepos/piliplus.svg)](https://repology.org/project/piliplus/versions) ## refactor - [ ] gRPC [wip] - [x] 用户界面 - [x] 其他 ## feat - [x] 编辑动态 - [x] DLNA 投屏 - [x] 离线缓存/播放 - [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] 播放音频 - [x] 跳过番剧片头/片尾 - [x] 安卓端 `loudnorm` 适配 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] Win/Mac 支持极验、短信登录 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] 视频截取动图 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] AI 原声翻译 - [x] SuperChat - [x] 播放课堂视频 - [x] 发起投票 - [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户` - [x] 修改消息设置 - [x] 修改聊天设置 - [x] 展示折叠消息 - [x] 查看用户图文 - [x] 动态话题 - [x] 直播分区 - [x] 分享`视频`/`番剧`/`动态`/`专栏`/`直播`至消息 - [x] 创建/修改/删除关注分组 - [x] 移除粉丝 - [x] 直播弹幕发送表情 - [x] 收藏夹排序 - [x] 稍后再看 ~~`未看`~~ / `未看完` / ~~`已看完`~~ 分类 - [x] WebDAV 备份/恢复设置 - [x] 保存评论/动态 - [x] 高级弹幕 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] 取消/置顶评论 - [x] 记笔记 - [x] 多账号支持 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] 屏蔽带货动态/评论 - [x] 互动视频 - [x] 发评/动态反诈 - [x] 高能进度条 - [x] 滑动跳转预览视频缩略图 - [x] Live Photo - [x] 复制/移动/排序收藏夹/稍后再看视频 - [x] 超分辨率 - [x] 合并弹幕 - [x] 会员彩色弹幕 - [x] 播放全部/继续播放/倒序播放 - [x] Cookie登录 - [x] 显示视频分段信息 - [x] 调节字幕大小 - [x] 调节全屏弹幕大小 - [x] 收藏夹/稍后再看多选删除 - [x] 搜索用户动态 - [x] 直播弹幕 - [x] 修改头像/用户名/签名/性别/生日 - [x] 创建/编辑/删除收藏夹 - [x] 评论楼中楼查看对话 - [x] 评论楼中楼定位点击查看的评论 - [x] 评论楼中楼按热度/时间排序 - [x] 评论点踩 - [x] 私信发图 - [x] 投币动画 - [x] 取消/追番,更新追番状态 - [x] 取消/订阅合集 - [x] SponsorBlock - [x] 显示视频完整合集 - [x] 三连动画 - [x] 番剧三连 - [x] 带图评论 - [x] 视频TAG - [x] 筛选搜索 - [x] 转发动态 - [x] 合集图片 - [x] 删除/置顶/撤回私信 - [x] 举报用户/评论/视频/动态 - [x] 删除/发布/置顶文本/图片动态 - [x] 其他 ## opt - [x] 专栏界面 - [x] 私信界面 - [x] 收藏面板 - [x] PIP - [x] 视频封面 - [x] 回复界面 - [x] 系统通知 - [x] 评论显示 - [x] 亮度调节 - [x] 视频播放 - [x] 视频staff - [x] 防止bottomsheet遮挡全屏视频 - [x] 其他 ## fix - [x] 番剧分集点赞/投币/收藏 - [x] bugs
## 功能 - [x] 推荐视频列表(app端) - [x] 最热视频列表 - [x] 热门直播 - [x] 番剧列表 - [x] 屏蔽黑名单内用户视频 - [x] 无痕模式(播放视为未登录) - [x] 游客模式(推荐视为未登录) - [x] 用户相关 - [x] 粉丝、关注用户、拉黑用户查看 - [x] 用户主页查看 - [x] 关注/取关用户 - [x] 离线缓存 - [x] 稍后再看 - [x] 观看记录 - [x] 我的收藏 - [x] 站内私信 - [x] 动态相关 - [x] 全部、投稿、番剧分类查看 - [x] 动态评论查看 - [x] 动态评论回复功能 - [x] 视频播放相关 - [x] 双击快进/快退 - [x] 双击播放/暂停 - [x] 垂直方向调节亮度/音量 - [x] 垂直方向上滑全屏、下滑退出全屏 - [x] 水平方向手势快进/快退 - [x] 全屏方向设置 - [x] 倍速选择/长按2倍速 - [x] 硬件加速(视机型而定) - [x] 画质选择(高清画质未解锁) - [x] 音质选择(视视频而定) - [x] 解码格式选择(视视频而定) - [x] 弹幕 - [x] 字幕 - [x] 记忆播放 - [x] 视频比例:高度/宽度适应、填充、包含等 - [x] 搜索相关 - [x] 热搜 - [x] 搜索历史 - [x] 默认搜索词 - [x] 投稿、番剧、直播间、用户搜索 - [x] 视频搜索排序、按时长筛选 - [x] 视频详情页相关 - [x] 视频选集(分p)切换 - [x] 点赞、投币、收藏/取消收藏 - [x] 相关视频查看 - [x] 评论用户身份标识 - [x] 评论(排序)查看、二楼评论查看 - [x] 主楼、二楼评论回复功能 - [x] 评论点赞 - [x] 评论笔记图片查看、保存 - [x] 设置相关 - [x] 画质、音质、解码方式预设 - [x] 图片质量设定 - [x] 主题模式:亮色/暗色/跟随系统 - [x] 震动反馈(可选) - [x] 高帧率 - [x] 自动全屏 - [x] 横屏适配 - [ ] 等等
## 下载 可以通过右侧release进行下载或拉取代码到本地进行编译
## 声明 此项目(PiliPlus)是个人为了兴趣而开发,仅用于学习和测试,请于下载后24小时内删除。 所用API皆从官方网站收集,不提供任何破解内容。 在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala) 在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX) 本仓库做了更激进的修改,感谢原作者的开源精神。 感谢使用
## 致谢 - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) - [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer) - [media-kit](https://github.com/media-kit/media-kit) - [dio](https://pub.dev/packages/dio) - 等等


## Star History Star History Chart ================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml analyzer: exclude: - lib/grpc/bilibili/** # - lib/grpc/google/** # - lib/common/widgets/flutter/** formatter: trailing_commas: preserve linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. # https://dart.dev/tools/linter-rules rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # - always_specify_types # - avoid_positional_boolean_parameters - always_declare_return_types - always_use_package_imports - avoid_empty_else - avoid_field_initializers_in_const_classes - avoid_print - avoid_relative_lib_imports - avoid_shadowing_type_parameters - avoid_single_cascade_in_expression_statements - avoid_slow_async_io - avoid_type_to_string - avoid_types_as_parameter_names - avoid_unnecessary_containers - avoid_void_async - await_only_futures - camel_case_extensions - camel_case_types - cancel_subscriptions - cascade_invocations - prefer_const_constructors - prefer_const_declarations - sized_box_for_whitespace - unnecessary_late - use_colored_box - use_decorated_box - use_named_constants - use_null_aware_elements - unnecessary_lambdas - use_is_even_rather_than_modulo - unnecessary_async - unnecessary_await_in_return - unnecessary_getters_setters - prefer_const_literals_to_create_immutables - no_literal_bool_comparisons - use_truncating_division - use_string_buffers - unnecessary_statements - unnecessary_nullable_for_final_variable_declarations - tighten_type_of_initializing_formals - prefer_void_to_null - prefer_spread_collections - unnecessary_to_list_in_spreads - prefer_for_elements_to_map_fromIterable # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks /build /.kotlin ================================================ FILE: android/app/.gitignore ================================================ /.cxx /build ================================================ FILE: android/app/build.gradle.kts ================================================ import com.android.build.gradle.internal.api.ApkVariantOutputImpl import org.jetbrains.kotlin.konan.properties.Properties plugins { id("com.android.application") id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } android { namespace = "com.example.piliplus" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { applicationId = "com.example.piliplus" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } packagingOptions.jniLibs.useLegacyPackaging = true val keyProperties = Properties().also { val properties = rootProject.file("key.properties") if (properties.exists()) it.load(properties.inputStream()) } val config = keyProperties.getProperty("storeFile")?.let { signingConfigs.create("release") { storeFile = file(it) storePassword = keyProperties.getProperty("storePassword") keyAlias = keyProperties.getProperty("keyAlias") keyPassword = keyProperties.getProperty("keyPassword") enableV1Signing = true enableV2Signing = true } } buildTypes { all { signingConfig = config ?: signingConfigs["debug"] } release { if (project.hasProperty("dev")) { applicationIdSuffix = ".dev" resValue( type = "string", name = "app_name", value = "PiliPlus dev", ) } proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } debug { applicationIdSuffix = ".debug" } } applicationVariants.all { val variant = this variant.outputs.forEach { output -> (output as ApkVariantOutputImpl).versionCodeOverride = flutter.versionCode } } } flutter { source = "../.." } ================================================ FILE: android/app/proguard-rules.pro ================================================ -dontwarn javax.annotation.Nullable -dontwarn org.conscrypt.Conscrypt -dontwarn org.conscrypt.OpenSSLProvider ================================================ FILE: android/app/src/debug/res/values/string.xml ================================================ PiliPlus debug ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/example/piliplus/MainActivity.kt ================================================ package com.example.piliplus import android.app.PictureInPictureParams import android.app.SearchManager import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.provider.Settings import android.view.WindowManager.LayoutParams import androidx.core.net.toUri import com.ryanheise.audioservice.AudioServiceActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import kotlin.system.exitProcess class MainActivity : AudioServiceActivity() { private lateinit var methodChannel: MethodChannel override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "PiliPlus") methodChannel.setMethodCallHandler { call, result -> when (call.method) { "back" -> back(); "biliSendCommAntifraud" -> { try { val action = call.argument("action") ?: 0 val oid = call.argument("oid") ?: 0L val type = call.argument("type") ?: 0 val rpid = call.argument("rpid") ?: 0L val root = call.argument("root") ?: 0L val parent = call.argument("parent") ?: 0L val ctime = call.argument("ctime") ?: 0L val commentText = call.argument("comment_text") ?: "" val pictures = call.argument("pictures") val sourceId = call.argument("source_id") ?: "" val uid = call.argument("uid") ?: 0L val cookies = call.argument>("cookies") ?: emptyList() val intent = Intent().apply { component = ComponentName( "icu.freedomIntrovert.biliSendCommAntifraud", "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity" ) putExtra("action", action) putExtra("oid", oid.toLong()) putExtra("type", type) putExtra("rpid", rpid.toLong()) putExtra("root", root.toLong()) putExtra("parent", parent.toLong()) putExtra("ctime", ctime.toLong()) putExtra("comment_text", commentText) if (pictures != null) putExtra("pictures", pictures) putExtra("source_id", sourceId) putExtra("uid", uid.toLong()) putStringArrayListExtra("cookies", ArrayList(cookies)) } startActivity(intent) } catch (_: Exception) { } } "linkVerifySettings" -> { val uri = ("package:" + context.packageName).toUri() try { val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri) } else { Intent("android.intent.action.MAIN", uri).setClassName( "com.android.settings", "com.android.settings.applications.InstalledAppOpenByDefaultActivity" ) } context.startActivity(intent) } catch (_: Throwable) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) context.startActivity(intent) } } "music" -> { val title = call.argument("title") val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply { putExtra(SearchManager.QUERY, title) putExtra(MediaStore.EXTRA_MEDIA_TITLE, title) call.argument("artist") ?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) } call.argument("album") ?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) } addCategory(Intent.CATEGORY_DEFAULT) } try { if (packageManager.resolveActivity( intent, PackageManager.MATCH_DEFAULT_ONLY ) != null ) { startActivity(intent) result.success(true) return@setMethodCallHandler } } catch (_: Throwable) { } try { intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH if (packageManager.resolveActivity( intent, PackageManager.MATCH_DEFAULT_ONLY ) != null ) { startActivity(intent) result.success(true) return@setMethodCallHandler } } catch (_: Throwable) { } result.success(false) } "setPipAutoEnterEnabled" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val params = PictureInPictureParams.Builder() .setAutoEnterEnabled(call.argument("autoEnable") ?: false) .build() setPictureInPictureParams(params) } } else -> result.notImplemented() } } } private fun back() { val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_HOME) flags = Intent.FLAG_ACTIVITY_NEW_TASK } startActivity(intent) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { window.attributes.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } } override fun onDestroy() { stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java)) super.onDestroy() android.os.Process.killProcess(android.os.Process.myPid()) exitProcess(0) } override fun onUserLeaveHint() { super.onUserLeaveHint() methodChannel.invokeMethod("onUserLeaveHint", null) } override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration? ) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) MethodChannel( flutterEngine!!.dartExecutor.binaryMessenger, "floating" ).invokeMethod("onPipChanged", isInPictureInPictureMode) } } ================================================ FILE: android/app/src/main/res/drawable/ic_baseline_forward_10_24.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_baseline_replay_10_24.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/ic_notification_icon.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/raw/keep.xml ================================================ ================================================ FILE: android/app/src/main/res/values/colors.xml ================================================ #FF5CB67B #FFFFFFFF ================================================ FILE: android/app/src/main/res/values/string.xml ================================================ PiliPlus ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night-v31/colors.xml ================================================ @android:color/system_accent1_100 @android:color/system_neutral1_800 ================================================ FILE: android/app/src/main/res/values-night-v31/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-v31/colors.xml ================================================ @android:color/system_neutral2_700 @android:color/system_accent1_100 ================================================ FILE: android/app/src/main/res/values-v31/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile allprojects { repositories { google() mavenCentral() } } val newBuildDir: Directory = rootProject.layout.buildDirectory .dir("../../build") .get() rootProject.layout.buildDirectory.value(newBuildDir) subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } subprojects { afterEvaluate { if (project.extensions.findByName("android") != null) { val androidExtension = project.extensions.getByName("android") as com.android.build.gradle.BaseExtension if (androidExtension.namespace == null) { androidExtension.namespace = project.group.toString() } androidExtension.compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } project.tasks.withType().configureEach { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } val pluginCompileSdkStr = androidExtension.compileSdkVersion val pluginCompileSdk = pluginCompileSdkStr ?.removePrefix("android-") ?.toIntOrNull() if (pluginCompileSdk != null && pluginCompileSdk < 31) { project.logger.error( "Warning: Overriding compileSdk version in Flutter plugin: ${project.name} " + "from $pluginCompileSdk to 31 (to work around https://issuetracker.google.com/issues/199180389).\n" + "If there is not a new version of ${project.name}, consider filing an issue against ${project.name} " + "to increase their compileSdk to the latest (otherwise try updating to the latest version)." ) androidExtension.setCompileSdkVersion(31) } } project.buildDir = File(rootProject.buildDir, project.name) } } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean") { delete(rootProject.layout.buildDirectory) } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle.kts ================================================ pluginManagement { val flutterSdkPath = run { val properties = java.util.Properties() file("local.properties").inputStream().use { properties.load(it) } val flutterSdkPath = properties.getProperty("flutter.sdk") require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } flutterSdkPath } includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.12.1" apply false id("org.jetbrains.kotlin.android") version "2.2.20" apply false } include(":app") ================================================ FILE: assets/linux/DEBIAN/control ================================================ Package: PiliPlus Version: version_need_change Maintainer: gh-MzA4Nzk Original-Maintainer: bggRGjQaUbCoE Section: x11 Priority: optional Architecture: amd64 Essential: no Installed-Size: size_need_change Description: third-party Bilibili client developed in Flutter Homepage: https://github.com/bggRGjQaUbCoE/PiliPlus Depends: libgtk-3-0t64, libmpv2, gir1.2-ayatanaappindicator3-0.1, libayatana-appindicator3-1 ================================================ FILE: assets/linux/DEBIAN/postinst ================================================ #!/usr/bin/env bash ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus chmod +x /usr/bin/piliplus if [ $1 == "configure" ] && [ -x /usr/bin/update-mime-database ]; then echo "updating mime database..." update-mime-database /usr/share/mime || true fi if [ $1 == "configure" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then echo "updating icon cache..." gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true fi if [ $1 == "configure" ] && [ -x /usr/bin/update-desktop-database ]; then echo "configure desktop database..." update-desktop-database -q /usr/share/applications || true fi exit 0 ================================================ FILE: assets/linux/DEBIAN/postrm ================================================ #!/usr/bin/env bash rm /usr/bin/piliplus if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then if [ -x /usr/bin/update-desktop-database ]; then echo "updating desktop database..." update-desktop-database -q /usr/share/applications || true fi if [ -x /usr/bin/gtk-update-icon-cache ]; then echo "updating icon cache..." gtk-update-icon-cache -q -t /usr/share/icons/hicolor || true fi if [ -x /usr/bin/update-mime-database ]; then echo "updating mime database..." update-mime-database /usr/share/mime || true fi fi if [ $1 = "purge" ]; then echo "Removing user data..." rm -rf /home/*/.local/share/com.example.PiliPlus || true rm -rf /root/.local/share/com.example.PiliPlus || true fi exit 0 ================================================ FILE: assets/linux/DEBIAN/prerm ================================================ #!/usr/bin/env bash if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then echo "Stopping PiliPlus if running..." pkill -x piliplus || true fi exit 0 ================================================ FILE: assets/linux/com.example.piliplus.desktop ================================================ [Desktop Entry] Type=Application Name=PiliPlus Comment=A third-party Bilibili Client developed in Flutter Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端 Exec=piliplus Icon=piliplus Terminal=false StartupWMClass=com.example.piliplus Categories=Video;AudioVideo;Player; ================================================ FILE: assets/shaders/Anime4K_AutoDownscalePre_x2.glsl ================================================ // This is free and unencumbered software released into the public domain. // Anyone is free to copy, modify, publish, use, compile, sell, or // distribute this software, either in source code form or as a compiled // binary, for any purpose, commercial or non-commercial, and by any // means. // In jurisdictions that recognize copyright laws, the author or authors // of this software dedicate any and all copyright interest in the // software to the public domain. We make this dedication for the benefit // of the public at large and to the detriment of our heirs and // successors. We intend this dedication to be an overt act of // relinquishment in perpetuity of all present and future rights to this // software under copyright law. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // For more information, please refer to //!DESC Anime4K-v4.0-AutoDownscalePre-x2 //!HOOK MAIN //!BIND HOOKED //!BIND NATIVE //!WHEN OUTPUT.w NATIVE.w / 2.0 < OUTPUT.h NATIVE.h / 2.0 < * OUTPUT.w NATIVE.w / 1.2 > OUTPUT.h NATIVE.h / 1.2 > * * //!WIDTH OUTPUT.w //!HEIGHT OUTPUT.h vec4 hook() { return HOOKED_tex(HOOKED_pos); } ================================================ FILE: assets/shaders/Anime4K_AutoDownscalePre_x4.glsl ================================================ // This is free and unencumbered software released into the public domain. // Anyone is free to copy, modify, publish, use, compile, sell, or // distribute this software, either in source code form or as a compiled // binary, for any purpose, commercial or non-commercial, and by any // means. // In jurisdictions that recognize copyright laws, the author or authors // of this software dedicate any and all copyright interest in the // software to the public domain. We make this dedication for the benefit // of the public at large and to the detriment of our heirs and // successors. We intend this dedication to be an overt act of // relinquishment in perpetuity of all present and future rights to this // software under copyright law. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // For more information, please refer to //!DESC Anime4K-v3.2-AutoDownscalePre-x4 //!HOOK MAIN //!BIND HOOKED //!BIND NATIVE //!WHEN OUTPUT.w NATIVE.w / 4.0 < OUTPUT.h NATIVE.h / 4.0 < * OUTPUT.w NATIVE.w / 2.4 > OUTPUT.h NATIVE.h / 2.4 > * * //!WIDTH OUTPUT.w 2 / //!HEIGHT OUTPUT.h 2 / vec4 hook() { return HOOKED_tex(HOOKED_pos); } ================================================ FILE: assets/shaders/Anime4K_Clamp_Highlights.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-De-Ring-Compute-Statistics //!HOOK MAIN //!BIND HOOKED //!SAVE STATSMAX //!COMPONENTS 1 #define KERNELSIZE 5 //Kernel size, must be an positive odd integer. #define KERNELHALFSIZE 2 //Half of the kernel size without remainder. Must be equal to trunc(KERNELSIZE/2). float get_luma(vec4 rgba) { return dot(vec4(0.299, 0.587, 0.114, 0.0), rgba); } vec4 hook() { float gmax = 0.0; for (int i=0; iRGB matrix has 1 for every row... (which is the case for BT.709) //Otherwise we would need to convert RGB to YUV, modify Y then convert back to RGB. return HOOKED_tex(HOOKED_pos) - (current_luma - new_luma); } ================================================ FILE: assets/shaders/Anime4K_Restore_CNN_M.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(-0.09991986, 0.13782342, -0.031251684, -0.06356843, -0.3437488, 0.05450952, 0.34347802, 0.46335372, 0.08607224, 0.044988394, 0.137179, 0.17976908, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(-0.024212424, -0.09278509, -0.00040907756, 0.34552294, -0.13254678, 0.113105185, 0.005667946, -0.00036919137, -0.06375679, 0.009184115, 0.115518734, -0.115506776, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(-0.14101827, 0.023523493, 0.044094566, -0.019271746, -0.44348842, -0.08818877, -0.4026149, -0.21995795, -0.15880394, -0.013732858, -0.020751135, 0.012719151, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(0.013001821, -0.34503505, 0.39219138, 0.18792126, 0.24760444, -0.016173402, 0.10154511, 0.15453082, -0.058132876, 0.016784398, -0.05808539, -0.11039915, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(0.37024534, 0.041440863, -0.3374568, -0.44994286, 0.19555596, 0.20855539, -0.27974075, -0.5372628, 0.21228147, -0.0295346, -0.56700057, 0.030042822, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(-0.12940632, 0.057526, 0.090682045, -0.06985033, -0.13704006, -0.047685407, 0.44615674, -0.48056605, -0.06166251, -0.01883519, 0.2032237, -0.113287605, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(0.010856669, -0.35820737, 0.16757219, 0.082619876, -0.03967303, 0.038705572, 0.32652855, -0.012030017, 0.015120559, -0.15314877, 0.23442009, 0.09767922, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(-0.046272673, -0.17752305, 0.082018286, -0.2512824, 0.58619463, -0.060903464, -0.022793597, 0.077803515, -0.17025311, 0.05136993, 0.029383298, -0.15475409, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(-0.11212024, 0.13378005, -0.2027488, 0.08056421, -0.11176219, -0.048429377, -0.08396386, 0.10507829, 0.13326839, 0.0430627, 0.051362377, 0.06482755, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(-0.061233472, 0.39222646, 0.029704979, 0.02586828); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.16410656, -0.40521824, 0.13121907, -0.02314597, 0.105412476, -0.060401272, -0.043063477, -0.13933973, 0.12558138, -0.020861467, 0.030370515, 0.13178016, -0.14220351, 0.20736893, 0.003321564, -0.29241714) * go_0(-1.0, -1.0); result += mat4(0.18517321, 0.29162985, -0.26783395, 0.039760686, 0.025527012, -0.067319244, 0.055004176, 0.048916563, 0.12750523, -0.091435954, 0.13818842, 0.36704224, 0.0839921, 0.10186618, -0.17237376, 0.13282418) * go_0(-1.0, 0.0); result += mat4(-0.1657887, 0.0131325135, -0.17222486, 0.091398895, -0.12756164, -0.08437298, -0.29052997, 0.3269337, 0.15870757, -0.013529402, -0.0581753, 0.11802371, 0.07099966, -0.024063632, 0.31834844, -0.11183859) * go_0(-1.0, 1.0); result += mat4(0.46036887, -0.07654623, 0.22923063, 0.17463821, 0.10555414, -0.117430426, 0.12406777, -0.011399492, 0.028316498, 0.13684341, 0.009664087, 0.2022659, 0.04953974, -0.31342217, -0.6103131, -0.13605757) * go_0(0.0, -1.0); result += mat4(0.03406955, -0.39819366, 0.61176, -0.46809456, -0.029321073, 0.46619493, 0.36700186, 0.02288561, 0.11464085, -0.10931452, -0.09154022, 0.07334147, -0.5609916, 0.31826234, -0.011012659, -0.46719545) * go_0(0.0, 0.0); result += mat4(-0.056855045, 0.27037027, -0.09269696, -0.563572, -0.06816116, -0.22986612, 0.08693167, -0.16246101, 0.09954046, -0.05374176, 0.0071916827, -0.1788692, 0.3825241, -0.1609887, 0.055204768, 0.10213068) * go_0(0.0, 1.0); result += mat4(0.0646626, 0.102358796, -0.45055822, 0.20557903, -0.23337309, 0.12633002, -0.19299199, -0.15085731, -0.13473304, 0.053790465, -0.10061193, -0.13393497, -0.04264752, -0.029740738, -0.07865285, 0.20883279) * go_0(1.0, -1.0); result += mat4(0.010471527, -0.033218473, -0.46157447, 0.004866583, 0.23226471, -0.059343327, -0.1439596, 0.13619648, 0.013839963, 0.15930325, 0.043742355, 0.17467323, 0.33772305, 0.40261495, -0.08351293, 0.18129359) * go_0(1.0, 0.0); result += mat4(-0.12493434, -0.1875134, -0.074943796, -0.0031701606, -0.037142616, 0.1667002, 0.16665547, -0.011248127, 0.0071619414, 0.0034872112, 0.120318964, -0.09625579, 0.14917047, -0.16310586, 0.07231737, 0.30447328) * go_0(1.0, 1.0); result += mat4(0.093798615, 0.17074613, -0.08780678, -0.012520207, 0.118534856, 0.027508778, -0.2778478, -0.19509242, -0.34137097, 0.32000312, -0.22027159, 0.337515, 0.16220862, 0.108993016, 0.14070526, 0.12784284) * go_1(-1.0, -1.0); result += mat4(-0.14325632, -0.1467453, -0.27502358, 0.09370837, 0.11821083, -0.012266484, -0.2100548, 0.4707502, -0.06766648, 0.58165014, -0.2512279, -0.33783755, 0.1318925, -0.04346277, 0.15454485, 0.044500057) * go_1(-1.0, 0.0); result += mat4(-0.05683207, 0.0051946463, -0.108000524, 0.10133204, -0.50763863, 0.007308442, 0.8542404, 0.28387356, 0.022709515, 0.294523, -0.3822472, 0.66166407, 0.01404485, 0.031282708, -0.26756814, -0.123147786) * go_1(-1.0, 1.0); result += mat4(-0.36455178, 0.3470555, -0.045303088, -0.03170764, -0.15802494, -0.0019141496, -0.25939587, -0.23875342, 0.130428, 0.03954273, -0.17985536, 0.105145946, 0.15804817, 0.12551713, 0.28371975, -0.085748516) * go_1(0.0, -1.0); result += mat4(0.0060625463, 0.2443924, -0.017692259, -0.20214005, -0.09584515, -0.012805372, -0.13942227, 0.16143198, 0.12942013, 0.41785547, 0.046071563, 0.7030026, 0.10499644, -0.20566013, -0.031321276, 0.27830327) * go_1(0.0, 0.0); result += mat4(-0.081274964, -0.14562319, 0.27200526, -0.20491314, 0.012910989, 0.024201397, 0.04816258, 0.21297328, -0.22015952, -0.44160756, -0.056035373, 0.33824417, -0.31645304, 0.15469243, 0.053187452, -0.20989445) * go_1(0.0, 1.0); result += mat4(-0.046550367, 0.033185404, 0.33337244, 0.12853645, 0.23520172, -0.05909214, 0.0861368, 0.10706329, -0.07058717, -0.11759937, -0.18594047, 0.080006264, -0.055425353, -0.12506317, 0.15729053, -0.0915004) * go_1(1.0, -1.0); result += mat4(0.042516407, 0.14844789, 0.16533111, 0.13502933, -0.0655417, -0.057256397, 0.076713726, -0.23448966, 0.12855926, 0.014219275, 0.051761385, 0.053433083, -0.2446715, -0.4008074, 0.19603717, -0.1796951) * go_1(1.0, 0.0); result += mat4(0.14777803, 0.15524907, 0.043158617, -0.06996876, 0.19210646, -0.2144364, -0.47020787, -0.4207906, -0.18074386, -0.2163903, 0.0030754965, 0.36799973, -0.3837698, -0.0022661497, -0.37276733, -0.28934997) * go_1(1.0, 1.0); result += vec4(-0.018297346, -0.080951825, -0.062163066, -0.08050014); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.31543177, 0.23095237, -0.06692611, -0.5867763, 0.003622504, 0.17948842, -0.14627707, 0.1745016, -0.052964583, -0.15551159, 0.05644786, -0.012665164, 0.13107763, 0.11369179, -0.09452995, -0.11973403) * go_0(-1.0, -1.0); result += mat4(-0.2694661, -0.115382135, 0.3073268, -0.067228466, -0.25511482, -0.13922207, 0.36758214, -0.18821828, -0.022617863, 0.20333402, -0.11125889, 0.3552245, -0.013346653, -0.099095374, -0.25100616, 0.35521755) * go_0(-1.0, 0.0); result += mat4(0.011012409, -0.13675085, 0.25642, -0.34851208, -0.23184675, 0.18012202, 0.57654136, 0.103173524, -0.16461405, 0.038177088, 0.1234096, 0.013202029, -0.19033363, 0.07469178, -0.017948546, 0.15287702) * go_0(-1.0, 1.0); result += mat4(-0.05340533, 0.23797482, 0.20351392, -0.05333351, -0.12181174, -0.23363493, -0.20696607, 0.109941036, -0.11519453, 0.13842066, -0.10687832, 0.29040006, 0.022218632, 0.031238724, 0.2685182, 0.15300068) * go_0(0.0, -1.0); result += mat4(0.22985318, -0.3103802, -0.22916415, 0.25238806, -0.11690287, -0.1947488, 0.118020535, 0.07814263, -0.06335474, -0.007870727, 0.076106325, 0.094677486, -0.16776285, -0.006570437, -0.29589584, 0.41413507) * go_0(0.0, 0.0); result += mat4(0.43607962, -0.36456433, -0.123776875, -0.16634953, -0.091190875, 0.13035081, 0.28627968, 0.27249968, 0.12356344, -0.008616177, 0.09599816, -0.006144557, -0.23490307, 0.3013123, 0.14153156, 0.21837278) * go_0(0.0, 1.0); result += mat4(0.060364585, 0.37860224, 0.039182413, -0.22805426, -0.089910224, -0.06817697, -0.2684275, -0.12528503, 0.036934495, -0.07826616, 0.06559976, -0.08253646, 0.13489649, 0.06237663, 0.126376, 0.21194184) * go_0(1.0, -1.0); result += mat4(-0.12534817, 0.21225189, -0.27818045, -0.3070443, -0.006957577, -0.025105853, 0.12100924, -0.06916452, 0.23081483, 0.1802756, -0.18995638, 0.16603014, -0.2904096, -0.25292823, -0.21834068, 0.13719653) * go_0(1.0, 0.0); result += mat4(0.017209655, 0.10757137, 0.21414296, -0.30885983, 0.10467716, -0.2184891, 0.100061476, -0.1527528, 0.2100472, -0.25768545, -0.22329919, -0.29153427, -0.06983842, -0.103854865, -0.051384352, 0.14629121) * go_0(1.0, 1.0); result += mat4(0.0059623295, -0.26060802, 0.32115817, 0.021025505, 0.09783085, -0.15865178, 0.1473021, -0.24977303, -0.033508282, 0.17480391, -0.091310136, 0.09870876, 0.10504043, -0.06105686, 0.013493489, -0.11278855) * go_1(-1.0, -1.0); result += mat4(0.14875248, -0.14859414, 0.19377062, -0.17456068, 0.101288855, -0.1113682, -0.48944646, 0.1018565, -0.037392337, 0.08539691, 0.1751306, -0.15428723, -0.059375558, 0.027663672, 0.051804014, -0.049813222) * go_1(-1.0, 0.0); result += mat4(0.118846565, -0.19869871, -0.037388258, 0.08456728, -0.11662527, -0.43818352, -0.093285345, 0.038507205, -0.051991668, 0.21008292, 0.10792365, 0.2020924, 0.057021596, 0.09460527, 0.0016551288, -0.0015957063) * go_1(-1.0, 1.0); result += mat4(0.11062174, -0.2639232, -0.060295466, -0.3217331, -0.050545212, 0.30989558, 0.30906132, 0.030323273, 0.028986752, 0.037429404, 0.20855664, -0.19848943, 0.034687653, -0.09599135, -0.06250494, -0.13215867) * go_1(0.0, -1.0); result += mat4(-0.010391146, 0.07657845, 0.44491258, 0.0435906, 0.0075931503, 0.42632654, 0.47022533, 0.34737435, -0.15452717, -0.14613411, -0.45231065, 0.12094409, 0.0067911847, 0.057501152, 0.09876979, 0.044946447) * go_1(0.0, 0.0); result += mat4(-0.15607435, 0.2293058, -0.09520331, 0.012836732, -0.15282455, 0.26437718, -0.1685477, -0.13211122, -0.055801593, -0.016778728, -0.34478986, -0.23228309, 0.12300962, -0.13235827, -0.13987203, -0.16550972) * go_1(0.0, 1.0); result += mat4(0.13161735, -0.09039346, -0.033475474, -0.23686698, 0.1514885, 0.20977421, 0.031431954, -0.0049226107, 0.090661936, 0.15288061, -0.03316583, 0.09646573, -0.32651708, 0.18825398, -0.15777239, 0.17572704) * go_1(1.0, -1.0); result += mat4(0.112157226, -0.08712878, 0.23453182, 0.1043877, -0.14686783, 0.28682423, -0.086443506, 0.059457052, -0.31530112, -0.2700583, -0.06028952, -0.070416875, 0.18053482, 0.16653341, 0.25215197, 0.061915852) * go_1(1.0, 0.0); result += mat4(-0.20122242, 0.076313145, -0.0988483, 0.094337784, -0.35436687, 0.3762327, -0.07809558, 0.3055848, 0.10425242, -0.17087407, 0.030301496, -0.13911743, 0.01630275, 0.24247427, -0.006474477, 0.03842641) * go_1(1.0, 1.0); result += vec4(-0.008952847, -0.0058945753, -0.08097229, 0.020968592); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_2_tf //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.2237721, -0.0064096362, -0.31808427, 0.73477733, 0.015353088, 0.23983319, 0.14967978, -0.34920225, -0.07456269, 0.093151815, -0.14331086, -0.24586205, -0.14183366, 0.06401045, -0.22044073, 0.29932275) * go_0(-1.0, -1.0); result += mat4(-0.07968509, -0.3349146, 0.16529128, 0.08443499, 0.4095855, -0.17120704, 0.17425705, 0.15298946, 0.2981273, 0.2212369, 0.10392389, -0.28775454, -0.065247655, -0.15255849, 0.13094437, 0.18685219) * go_0(-1.0, 0.0); result += mat4(0.015706737, -0.17755036, 0.2622526, 0.112057306, -0.15876788, -0.38466996, -0.33700845, -0.031711742, -0.023320962, -0.3145249, -0.21223734, -0.1314596, -0.1888095, -0.046370104, 0.09000896, -0.0046378844) * go_0(-1.0, 1.0); result += mat4(-0.31127506, 0.31304324, -0.03965752, 0.03649018, -0.029851055, 0.05801377, 0.00040150844, -0.04422069, 0.18019931, 0.14415511, -0.09845236, 0.21895434, -0.013932474, -0.046454947, -0.3403935, -0.006705289) * go_0(0.0, -1.0); result += mat4(-0.34878647, -0.5129283, 0.060250953, -0.16354133, 0.20644619, 0.08732273, -0.24118888, 0.24455065, 0.24449423, 0.44103387, 0.22455928, 0.25738943, -0.26914698, -0.21309987, 0.08386486, 0.021484816) * go_0(0.0, 0.0); result += mat4(-0.057454903, -0.4121922, 0.022661546, 0.37178272, 0.03331408, 0.05044008, 0.04324371, 0.20727943, 0.2432641, 0.076906696, -0.20858039, 0.012439015, -0.19335061, 0.09217451, 0.1968369, -0.19435833) * go_0(0.0, 1.0); result += mat4(-0.16960496, 0.24616167, 0.37977478, 0.14324574, -0.011531225, -0.11312143, -0.18141079, -0.23843932, 0.0086012175, -0.3564491, -0.12639481, 0.009799298, -0.29120612, 0.23756824, 0.18035695, -0.087133996) * go_0(1.0, -1.0); result += mat4(-0.10081239, 0.29191494, 0.10434693, 0.08970636, 0.008997759, 0.104756236, 0.039641086, 0.02323888, -0.11627765, 0.023693223, -0.30801758, -0.120208986, 0.05086147, 0.18498175, 0.15595439, -0.09877306) * go_0(1.0, 0.0); result += mat4(0.101321675, -0.2929976, 0.38810417, 0.5605376, -0.04073937, 0.030110704, -0.18147062, -0.09833952, 0.01927733, 0.15335669, -0.15384074, -0.110595055, -0.054297395, -0.077522054, 0.07918369, -0.068480626) * go_0(1.0, 1.0); result += mat4(0.23263514, -0.11719232, 0.2903209, -0.007503795, -0.020222448, -0.17790157, -0.15600762, -0.08741775, 0.12529704, 0.25548857, -0.04585447, -0.10255033, 0.18350503, -0.29593533, 0.0868933, 0.027004737) * go_1(-1.0, -1.0); result += mat4(-0.14958654, -0.006238835, -0.2928948, 0.1988557, -0.17057803, 0.12524141, 0.13978264, -0.019280292, 0.05967142, -0.07790818, -0.5893818, -0.022845713, -0.08596779, 0.07875358, -0.03316667, -0.4369282) * go_1(-1.0, 0.0); result += mat4(0.19195688, -0.060883682, -0.25897828, 0.07063324, 0.090833396, 0.003422883, 0.109534174, 0.031180874, -0.05017118, 0.022862168, -0.270113, -0.057831235, 0.53920543, -0.10252776, -0.091807485, 0.004294343) * go_1(-1.0, 1.0); result += mat4(-0.18494242, -0.119284816, 0.3821897, 0.07777979, 0.15568028, -0.2854859, -0.22441281, -0.049155876, -0.15292497, 0.21895619, -0.095677756, 0.15210424, 0.001643022, -0.026176987, 0.048463076, -0.4824009) * go_1(0.0, -1.0); result += mat4(0.007215129, 0.17074333, 0.053930074, -0.027014816, -0.17180431, -0.15163863, -0.0012122132, -0.18934256, -0.08294297, -0.24580221, -0.46552867, -0.27923223, 0.4092668, 0.06288688, -0.1602188, -0.0030876845) * go_1(0.0, 0.0); result += mat4(0.111870885, 0.03317145, 0.14155298, 0.20328505, -0.05104131, 0.13979794, 0.018966835, -0.07238511, 0.05493792, -0.14975783, -0.10293237, -0.21985306, 0.49054706, 0.18288186, -0.26925826, 0.35845932) * go_1(0.0, 1.0); result += mat4(0.3747799, -0.096748486, -0.17139742, 0.25289854, -0.17421168, -0.018461818, 0.09747162, 0.01660535, -0.20580359, 0.56189656, 0.17151354, -0.26347768, 0.28350568, -0.21486014, -0.44330928, -0.008981037) * go_1(1.0, -1.0); result += mat4(0.10169985, -0.18244018, 0.04760736, 0.41017643, -0.09468786, -0.024218475, 0.103733875, -0.22540338, 0.10630112, 0.3677178, -0.104170956, 0.057317447, 0.21764882, 0.0789158, -0.22041337, 0.15065216) * go_1(1.0, 0.0); result += mat4(0.11633995, -0.008195114, -0.14501533, 0.07168025, 0.058413275, 0.055995367, 0.09362145, -0.13827963, 0.13760869, 0.040319785, 0.038895044, 0.2675253, -0.087339684, 0.1412073, -0.17166458, -0.2312994) * go_1(1.0, 1.0); result += vec4(-0.059377354, -0.02055341, 0.07234869, -0.015452986); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_3_tf //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.29012984, -0.13150147, 0.31015614, 0.05992291, -0.050289866, 0.14845313, -0.09608898, 0.27913308, 0.060307387, -0.04160452, 0.035932682, -0.08137563, -0.07999419, 0.11818284, -0.27512288, 0.21948813) * go_0(-1.0, -1.0); result += mat4(0.12916058, -0.21759962, -0.33868533, 0.021636661, 0.053470243, 0.1412425, 0.043395396, -0.26751056, -0.01689101, -0.2623835, 0.010809152, 0.062962815, -0.20692012, -0.1677863, -0.23313859, -0.17402615) * go_0(-1.0, 0.0); result += mat4(-0.08204112, -0.23672083, -0.0064437394, -0.13200696, -0.056692924, -0.02708657, 0.12536962, 0.004428919, 0.14137582, 0.15404348, -0.105753876, 0.047957454, 0.15734316, 0.16562423, -0.010160829, -0.06602983) * go_0(-1.0, 1.0); result += mat4(0.025653997, -0.10877775, -0.31258908, 0.18841636, -0.36005193, 0.1816357, -0.34537643, -0.0741087, 0.4663994, 0.0065186517, 0.08109033, 0.2976773, -0.35774228, -0.041366056, -0.37852773, 0.050565656) * go_0(0.0, -1.0); result += mat4(0.04392313, 0.11316681, -0.14421389, 0.17985669, -0.1651274, -0.5656209, -0.124100484, 0.42774054, -0.1153939, 0.16829851, 0.2025612, 0.054007456, -0.06868256, -0.56935954, -0.12227961, 0.17688861) * go_0(0.0, 0.0); result += mat4(0.34041, 0.499, 0.15234196, 0.21353458, -0.2732667, -0.049950935, 0.03550811, -0.21051687, 0.2609023, 0.016438454, -0.29874632, 0.37994128, 0.049288407, -0.31126305, 0.029235512, -0.012256015) * go_0(0.0, 1.0); result += mat4(-0.0046853204, 0.15391374, -0.040689662, 0.20186873, -0.08137621, 0.35905558, 0.23733845, 0.21794793, -0.066420384, 0.029600656, -0.31421044, -0.050773863, -0.06260773, 0.04634221, -0.10948491, -0.045498934) * go_0(1.0, -1.0); result += mat4(-0.082953, -0.025837064, -0.09928303, -0.14300232, 0.275064, 0.07793617, 0.22240888, 0.06637834, -0.4382666, -0.2932182, -0.27243167, -0.14221182, 0.5695728, 0.20719238, 0.5575927, 0.40816882) * go_0(1.0, 0.0); result += mat4(-0.18510929, -0.15052167, 0.25277212, 0.06804461, 0.016387, 0.20310035, 0.2903229, -0.0615877, -0.28987274, -0.11942605, 0.013498961, 0.3184152, 0.29543474, -0.042830903, -0.018111207, -0.13263674) * go_0(1.0, 1.0); result += mat4(0.25749087, 0.0053866603, -0.09391162, -0.06129529, -0.094091184, -0.07419633, 0.0013858611, 0.012000353, -0.062903, -0.0204224, -0.12113313, 0.017942557, -0.073379934, 0.052201986, 0.35864577, 0.023564404) * go_1(-1.0, -1.0); result += mat4(0.100115694, 0.19451359, 0.23252094, 0.19506809, -0.12470779, 0.0027281935, -0.17488572, -0.018721964, -0.15159339, 0.18457152, 0.057712987, -0.08191495, 0.19735703, 0.07326743, -0.28563106, 0.01642815) * go_1(-1.0, 0.0); result += mat4(0.068062514, 0.28356665, 0.07377898, 0.42776972, 0.28725025, -0.13045293, -0.17525704, -0.05885591, -0.16676305, -0.2555945, -0.10078422, -0.053032875, 0.084470876, 0.06460686, 0.13824362, -0.05231353) * go_1(-1.0, 1.0); result += mat4(0.22637829, -0.028969254, 0.1968254, -0.13331996, 0.038017053, -0.008854481, -0.2031639, 0.09237089, -0.3821112, 0.1108527, -0.11029933, -0.24542028, 0.22416145, -0.031492114, -0.19144306, -0.0996271) * go_1(0.0, -1.0); result += mat4(0.10776744, 0.16363445, 0.14656505, -0.3737814, -0.06642015, 0.5616549, -0.008412252, -0.37266847, 0.12506576, -0.15329036, 0.037538245, -0.10810259, 0.01706349, 0.1813702, 0.035651788, -0.012786579) * go_1(0.0, 0.0); result += mat4(-0.4023338, -0.2098614, -0.18285121, -0.02727653, 0.26107362, 0.041306913, -0.036515504, -0.045217298, -0.39958602, -0.21229339, -0.021053292, -0.13427502, 0.36178818, 0.20934913, 0.1500852, 0.2634554) * go_1(0.0, 1.0); result += mat4(0.07794611, -0.25937587, -0.06822529, -0.056336135, 0.094220124, 0.21588847, -0.0455218, -0.10968329, -0.08068449, -0.31366697, 0.07799637, 0.24252681, 0.23963861, 0.13715535, 0.010329345, 0.09094301) * go_1(1.0, -1.0); result += mat4(-0.20975718, -0.12550138, 0.14453574, -0.0020878632, -0.07153068, 0.3249998, -0.056577377, 0.18166828, 0.37204072, 0.17018336, 0.3752895, 0.32178587, 0.2571982, -0.27258632, -0.25971004, -0.40536007) * go_1(1.0, 0.0); result += mat4(-0.3243907, -0.06300621, -0.09398436, -0.19549188, 0.14906861, 0.061537784, -0.055284478, 0.11281728, 0.12964857, 0.09979093, -0.1810159, -0.4104283, 0.05807971, -0.056371246, 0.08072554, 0.18479007) * go_1(1.0, 1.0); result += vec4(-0.048888464, -0.0561434, 0.030690912, -0.030496685); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_4_tf //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.15332128, 0.027258258, 0.14900503, -0.15982795, 0.17021236, -0.51046044, -0.15287271, -0.058167327, 0.51826185, -0.34817994, 0.004513167, 0.05395769, 0.1990321, -0.049979225, 0.11391989, -0.16062729) * go_0(-1.0, -1.0); result += mat4(0.033682905, 0.019728886, 0.19931756, 0.17381927, 0.2585768, -0.2124572, -0.014632459, 0.39779893, -0.1146207, -0.2396625, 0.08960277, 0.38345298, 0.25497693, 0.11692859, -0.14207517, 0.12667973) * go_0(-1.0, 0.0); result += mat4(-0.14911255, 0.08910706, 0.16136818, 0.03914566, 0.24204038, -0.03607149, -0.4571109, 0.10802461, -0.0021356856, 0.00885878, 0.22297303, 0.2367231, 0.045177583, 0.11120606, -0.009971904, -0.059262395) * go_0(-1.0, 1.0); result += mat4(0.24565999, -0.2261384, 0.47373205, 0.024613412, -0.10923052, 0.039027315, -0.42707404, -0.3783373, 0.3544573, -0.5468578, -0.27599156, -0.09455918, 0.18760219, -0.19082001, 0.030565469, 0.20589156) * go_0(0.0, -1.0); result += mat4(0.1973198, -0.03433863, 0.059960485, 0.045642868, 0.1819595, -0.14460869, 0.1286175, 0.2067575, -0.042632047, -0.11842967, -0.11224446, -0.18764776, -0.19563004, 0.027425969, 0.24056377, 0.5949649) * go_0(0.0, 0.0); result += mat4(0.055027682, 0.16331595, -0.2608588, 0.12545955, 0.4588985, 0.03642909, 0.22187738, 0.45190734, -0.001210133, -0.057651415, -0.061199043, 0.11935476, -0.049561135, 0.27509886, 0.13778673, -0.124914035) * go_0(0.0, 1.0); result += mat4(-0.02257459, 0.27705106, 0.044165276, -0.26521233, 0.05982374, -0.2824302, 0.3171142, 0.08430561, -0.10155528, 0.16182268, -0.09183147, -0.19447176, 0.3295707, -0.50616395, -0.036964044, 0.23166709) * go_0(1.0, -1.0); result += mat4(-0.0232342, 0.07299799, -0.18038079, -0.13672702, -0.108305976, 0.15024792, -0.19531927, 0.0870979, -0.26488534, 0.19481428, 0.10737945, -0.14573483, -0.33094683, 0.24155116, -0.09850332, 0.2797003) * go_0(1.0, 0.0); result += mat4(-0.24089853, 0.19506595, 0.4799156, -0.058313113, 0.36212957, -0.44844806, 0.23864488, 0.15477742, -0.07795971, -0.0033861927, -0.11216164, 0.033454563, -0.25893036, 0.23793478, -0.15769425, -0.00033481256) * go_0(1.0, 1.0); result += mat4(0.05772507, -0.1640253, -0.13499664, -0.20460358, -0.024399966, 0.14966168, -0.090857334, -0.039677754, 0.00036956606, -0.24236615, -0.053542696, -0.0049544116, 0.026651502, 0.39019194, -0.2742246, -0.061242323) * go_1(-1.0, -1.0); result += mat4(-0.016323274, -0.036179908, 0.029965919, 0.11151491, -0.00016685206, -0.29573023, 0.17996423, -0.20145437, 0.1324275, -0.18442132, -0.24618152, 0.061780427, -0.02770517, 0.28452995, 0.39804098, -0.1174389) * go_1(-1.0, 0.0); result += mat4(-0.025068847, -0.053328387, -0.27053785, 0.26866457, -0.09866204, 0.057677213, 0.01850112, -0.18014707, -0.13319959, -0.14411181, -0.26355243, -0.022209354, -0.05062645, -0.036771543, 0.13294417, -0.18458557) * go_1(-1.0, 1.0); result += mat4(-0.046194963, 0.038230438, -0.08993043, -0.07236354, 0.11031123, -0.16504908, -0.09517036, -0.16459833, -0.5279925, 0.12686682, -0.05726125, 0.055361677, 0.31593755, 0.027328093, 0.001839602, 0.30581662) * go_1(0.0, -1.0); result += mat4(0.08608678, 0.03168437, 0.007713377, -0.26140293, -0.1268983, 0.13395861, -0.069848835, -0.24080403, 0.018839337, -0.049821075, -0.21461345, -0.14168301, -0.0872339, 0.47096667, 0.022512507, 0.14860632) * go_1(0.0, 0.0); result += mat4(0.06293673, 0.22462969, 0.045494985, 0.021673543, 0.18227446, -0.2956555, 0.08010543, -0.01919729, -0.012190269, 0.241983, -0.046537094, -0.40094566, -0.3853647, 0.1081711, -0.16926058, 0.16138376) * go_1(0.0, 1.0); result += mat4(-0.14854589, -0.17625804, -0.10849075, 0.221543, 0.099971965, 0.13901573, 0.29464146, 0.020068526, 0.054358527, -0.10351705, -0.0062914286, 0.24127026, -0.16914125, 0.12729423, -0.18377453, -0.6452375) * go_1(1.0, -1.0); result += mat4(0.12603393, -0.10986093, 0.2314103, 0.16915044, -0.13619255, -0.09349073, 0.20594226, -0.34507084, 0.19077192, 0.052500796, 0.07185645, 0.029082738, -0.015576321, 0.08254907, -0.5501743, -0.38495848) * go_1(1.0, 0.0); result += mat4(0.09300796, -0.079218306, 0.46825135, -0.08735625, 0.06321122, 0.16234867, 0.042932414, -0.013057422, 0.09697148, 0.23457524, 0.19417483, -0.16804664, 0.18379296, 0.17770062, -0.050235, -0.059676602) * go_1(1.0, 1.0); result += vec4(0.011169491, 0.032399546, 0.138099, 0.023857072); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_5_tf //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.22753362, -0.08612073, 0.33140692, 0.08699529, -0.18788953, -0.056579117, -0.12905197, -0.06694621, 0.054559365, 0.15031597, -0.13430363, 0.021646025, 0.14884405, -0.0694291, 0.26149413, 0.11270503) * go_0(-1.0, -1.0); result += mat4(0.17876762, -0.09637848, 0.11285323, 0.2004893, 0.1317187, -0.036162686, 0.17958368, -0.069625, 0.28760737, -0.12505141, 0.12760694, 0.047717955, -0.16811855, -0.16340709, 0.13278298, -0.08403954) * go_0(-1.0, 0.0); result += mat4(-0.21917523, 0.079711854, -0.28642535, 0.2822416, 0.03001489, -0.014772918, -0.3487396, 0.10597145, -0.013841082, 0.17034237, 0.10810282, -0.08089695, -0.22184245, -0.59067357, 0.44113398, 0.13045649) * go_0(-1.0, 1.0); result += mat4(-0.29906932, 0.013923749, 0.2031124, -0.11846688, -0.13953634, 0.08003455, -0.10164494, -0.21218559, 0.10563715, 0.31033117, -0.075903505, 0.047310907, -0.37824214, -0.14506383, 0.11866701, -0.21384487) * go_0(0.0, -1.0); result += mat4(-0.1353849, 0.19258606, 0.063908584, -0.2043788, 0.27244982, 0.1665306, -0.29357895, -0.22441709, 0.18514316, -0.17840464, 0.20986097, 0.14351055, -0.057732623, 0.42166704, -0.23182064, -0.4957248) * go_0(0.0, 0.0); result += mat4(-0.34830126, 0.109066755, -0.28285867, -0.048280068, -0.12290918, 0.04291651, -0.047484186, -0.03702595, 0.23047262, 0.09398974, 0.022467108, 0.08271034, 0.3066665, -0.54077, 0.057771873, 0.23194093) * go_0(0.0, 1.0); result += mat4(-0.17731948, -0.3175927, 0.1452728, 0.09396786, -0.16433562, -0.01833653, -0.22345604, -0.04161193, -0.14827462, 0.18544114, -0.15544125, -0.06179007, 0.16989979, -0.20985202, 0.16391534, -0.09447268) * go_0(1.0, -1.0); result += mat4(-0.053878862, -0.21034616, 0.023831524, 0.19772215, 0.31647214, 0.0126534775, -0.19130844, -0.049282108, -0.21446131, 0.067189045, 0.09117449, -0.25548774, 0.12109098, 0.22009392, -0.3924665, -0.13340388) * go_0(1.0, 0.0); result += mat4(-0.16096684, -0.18495405, 0.10410178, 0.0015673033, -0.00183498, -0.044303037, -0.062745355, -0.090802394, 0.043269135, 0.06924481, -0.21367405, -0.14619029, 0.11555763, -0.20292862, 0.5799557, 0.14739846) * go_0(1.0, 1.0); result += mat4(-0.21030277, -0.09578802, 0.013482288, -0.21484336, 0.12995781, 0.40431052, -0.3347856, -0.18183486, 0.15550353, -0.04402301, 0.4603779, 0.14874357, -0.07694621, -0.053523075, -0.19607326, -0.10850742) * go_1(-1.0, -1.0); result += mat4(-0.2347211, 0.2697403, -0.0634794, -0.17925987, 0.17231455, 0.24999185, -0.5208536, -0.10491828, -0.233575, 0.52950364, 0.0038063182, -0.1380038, 0.022935199, 0.19369157, 0.14586553, 0.1938704) * go_1(-1.0, 0.0); result += mat4(-0.10245223, 0.34150192, 0.25862157, -0.20165509, 0.5597771, 0.114510864, -0.122526556, -0.04010975, 0.1704679, -0.23335956, -0.16771887, -0.03783455, -0.056995615, 0.24153493, -0.08082429, -0.24210933) * go_1(-1.0, 1.0); result += mat4(-0.103466526, 0.15278348, -0.30526164, -0.080755696, 0.103505425, 0.15862796, 0.14696524, -0.008358076, -0.09180311, -0.12505089, 0.28052542, -0.13551563, 0.07528779, -0.09636086, -0.10369617, 0.23656134) * go_1(0.0, -1.0); result += mat4(-0.25752836, 0.099439755, -0.30716348, 0.035077725, 0.023509016, 0.23106368, 0.05277125, 0.34910464, 0.088015385, 0.26995596, 0.1390645, -0.40671825, 0.18096298, -0.100688554, 0.5492049, 0.2482101) * go_1(0.0, 0.0); result += mat4(0.41411775, -0.107200556, -0.13813478, 0.13768874, 0.27137747, 0.06313619, -0.08522967, 0.03218302, -0.03166121, -0.3415683, -0.52242, -0.1741813, -0.36956537, 0.179129, -0.09742935, -0.11696616) * go_1(0.0, 1.0); result += mat4(-0.07975504, 0.17964838, 0.37122533, 0.16064765, 0.14309953, 0.29473078, 0.0926391, -0.22333665, 0.34612748, -0.3387473, 0.0077308523, -0.07239449, 0.18522519, -0.21297298, 0.11493978, 0.16117814) * go_1(1.0, -1.0); result += mat4(-0.17402779, 0.10023144, 0.11712206, 0.031971734, 0.18713303, 0.08736295, 0.013007052, -0.06943139, -0.20102951, -0.010721135, -0.2562522, 0.34877458, -0.13732676, -0.40258047, 0.25824392, 0.15720639) * go_1(1.0, 0.0); result += mat4(0.044494305, 0.3296108, 0.0017603852, 0.09362289, 0.38839245, 0.40015858, -0.13395199, -0.044521853, -0.56266373, 0.251378, 0.5005789, -0.13106057, -0.18491416, -0.046887, 0.067797676, -0.14694957) * go_1(1.0, 1.0); result += vec4(0.013687534, -0.08185164, -0.04755438, 0.290178); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-3x1x1x56 //!HOOK MAIN //!BIND MAIN //!BIND conv2d_tf //!BIND conv2d_1_tf //!BIND conv2d_2_tf //!BIND conv2d_3_tf //!BIND conv2d_4_tf //!BIND conv2d_5_tf //!BIND conv2d_6_tf //!SAVE MAIN //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_2 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_5 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_6 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_8 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_9 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_10 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_12 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_13 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) vec4 hook() { vec4 result = mat4(-0.08837163, -0.065234736, -0.034704313, 0.0, 0.021405501, 0.013663729, 0.019249594, 0.0, 0.05328863, 0.03580334, 0.046457592, 0.0, -0.12216048, 0.022547891, 0.016400825, 0.0) * g_0; result += mat4(0.061996464, 0.05631466, 0.06808407, 0.0, -0.005013109, -0.0044589997, -0.032367796, 0.0, 0.016481603, 0.13721058, 0.14924648, 0.0, 0.020035887, -0.07250003, -0.08034037, 0.0) * g_1; result += mat4(0.24078514, 0.081361525, 0.053420708, 0.0, -0.009353794, -0.051077116, -0.058007747, 0.0, -0.14071098, 0.01035966, 0.005308949, 0.0, -0.1489842, -0.06711817, -0.05552926, 0.0) * g_2; result += mat4(-0.13002375, 0.012733757, 0.017821986, 0.0, 0.17767483, 0.20204604, 0.1751779, 0.0, 0.12804912, 0.07381453, 0.05655911, 0.0, 0.17044514, 0.07301451, 0.06523978, 0.0) * g_3; result += mat4(-0.1170986, -0.05130371, -0.027939914, 0.0, -0.16645707, -0.121526904, -0.09471366, 0.0, -0.04143118, 0.026693767, 0.034615446, 0.0, -0.084318705, -0.064990036, -0.054324172, 0.0) * g_4; result += mat4(0.12094524, 0.09518409, 0.07387219, 0.0, 0.062216382, 0.053228356, 0.031372335, 0.0, 0.072797105, 0.026258165, 0.009804673, 0.0, 0.120719045, 0.073281154, 0.056623302, 0.0) * g_5; result += mat4(-0.11141495, -0.11566289, -0.10398725, 0.0, -0.0651895, -0.06820691, -0.054204144, 0.0, -0.032746475, -0.008849683, -0.007610222, 0.0, -0.024655705, -0.048778858, -0.041144755, 0.0) * g_6; result += mat4(0.058090195, 0.07538767, 0.059722915, 0.0, 0.044788487, 0.04212742, 0.027502589, 0.0, 0.04892866, 0.015416752, 0.008312418, 0.0, -0.011864114, -0.0074752793, -0.0060824654, 0.0) * g_7; result += mat4(0.043446552, 0.061971307, 0.05758086, 0.0, -0.06379154, -0.053758245, -0.047204215, 0.0, 0.016307736, 0.03423424, 0.030179083, 0.0, 0.041445345, 0.03843772, 0.033059113, 0.0) * g_8; result += mat4(-0.003803544, 0.0008906116, -0.00059585314, 0.0, 0.102071285, 0.11485224, 0.10007254, 0.0, -0.074306004, -0.08803551, -0.07972321, 0.0, -0.030704215, -0.021514274, -0.009049376, 0.0) * g_9; result += mat4(0.0066058086, 0.0011408008, 0.0016199006, 0.0, -0.03916473, -0.042929266, -0.04018418, 0.0, -0.03153446, -0.039413508, -0.034767237, 0.0, 0.113516055, 0.12577052, 0.113335624, 0.0) * g_10; result += mat4(0.02655948, 0.041905303, 0.03861737, 0.0, 0.048471425, 0.049788587, 0.050447535, 0.0, 0.12092813, 0.13564217, 0.12613249, 0.0, -0.0023508538, 0.0012828974, 0.0028730957, 0.0) * g_11; result += mat4(0.0084758485, 0.008800083, 0.008206044, 0.0, -0.056123603, -0.06610845, -0.060320783, 0.0, -0.081793964, -0.101638645, -0.096699014, 0.0, -0.04402356, -0.04177539, -0.03829645, 0.0) * g_12; result += mat4(0.10676299, 0.118409514, 0.10618478, 0.0, -0.05880252, -0.06488367, -0.06432695, 0.0, 0.019221924, 0.017602798, 0.017413978, 0.0, -0.07512528, -0.080483615, -0.066218294, 0.0) * g_13; result += vec4(-0.010478934, -0.008364784, -0.010246552, 0.0); return result + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Restore_CNN_S.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(-0.19288683, -0.21397883, 0.111997396, -0.04791413, -0.26682988, -0.06144587, -0.03601853, -0.16693151, 0.038494494, -0.16651472, 0.147657, -0.083003886, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(-0.14286195, 0.08746566, -0.40107322, 0.12390977, -0.33392772, -0.18703035, -0.21326795, 0.04780781, -0.15155545, -0.0010025925, -0.1554875, -0.10676251, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(0.28095165, 0.022872915, -0.21342312, -0.29982176, 0.025937587, -0.055012174, -0.33779636, 0.0015666655, 0.076416336, 0.06656033, -0.1557806, 0.1078894, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(-0.31584853, 0.07527119, 0.30713862, -0.34014285, -0.50103146, -0.07217874, 0.512807, -0.09597398, -0.32097813, -0.051580857, -0.022466356, 0.01148551, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(-0.026032459, -0.04193211, 0.37703893, -0.031916667, -0.27421117, 1.0906446, -0.049654085, -0.19814016, 0.07819544, 0.06003738, 0.1405805, -0.0064135445, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(0.041450135, 0.11319654, -0.23237701, 0.08443178, 0.53344345, 0.30857387, -0.057264958, -0.1575803, 0.2325609, -0.027797326, -0.04544767, -0.18720597, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(0.2531829, -0.074966915, -0.27800754, -0.3146097, 0.20126024, -0.5380133, -0.15082566, -0.19021043, 0.29951036, 0.17123336, -0.01681872, -0.12574998, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(0.25203633, 0.19882993, 0.14906439, 0.13593598, 0.40712556, 0.084902965, 0.42969635, 0.2961132, -0.057267334, -0.030388135, 8.8084314e-05, 0.0210724, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(-0.13459359, -0.12199573, 0.12591946, 0.24736497, 0.2033463, -0.09388599, -0.094370656, 0.1071285, -0.18479438, -0.066625565, 0.08279283, 0.20130983, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(-0.011108127, -0.07481861, 0.07640154, 0.4964964); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.056432575, 0.0028165397, -0.026325442, -0.14802271, 0.16885762, -0.062179096, -0.2332292, 0.17513658, -0.08011296, 0.02947316, 0.014771492, -0.17946689, 0.026012989, -0.09823925, 0.036625937, -0.06924322) * go_0(-1.0, -1.0); result += mat4(-0.13571467, 0.09831142, 0.12911566, 0.06305893, -0.07188695, -0.20161287, 0.3858435, -0.21069056, -0.12294444, -0.1404628, -0.022659872, 0.23008968, 0.10969853, 0.17640765, 0.39796907, 0.20413099) * go_0(-1.0, 0.0); result += mat4(-0.0061665224, 0.055102807, -0.0059629944, -0.021429887, 0.061626043, 0.16898955, -0.21215646, 0.16510476, 0.2238265, 0.19429931, 0.09874656, 0.06828208, -0.122404456, -0.00026717107, -0.28203064, -0.29979932) * go_0(-1.0, 1.0); result += mat4(-0.22735378, 0.14538136, 0.11549746, 0.194148, -0.09841722, -0.0661309, 0.348576, -0.017375294, -0.044078812, 0.1298332, 0.04793373, -0.30687734, 0.08353025, 0.083519086, 0.10766399, 0.31796935) * go_0(0.0, -1.0); result += mat4(0.048365135, -0.17566709, -0.33212858, -0.052667376, -0.26443407, -0.010216014, 0.1573303, 0.05725314, 0.08140953, -0.09664591, 0.076109104, -0.026773714, 0.07732627, 0.10188082, -0.28266954, -0.16230233) * go_0(0.0, 0.0); result += mat4(0.29931107, 0.117944, -0.10414009, 0.12795551, 0.12576093, 0.17082554, -0.15803693, 0.13430743, -0.025801308, -0.10797019, 0.0721032, 0.2825884, -0.11025257, 0.12798019, 0.081827976, -0.050441865) * go_0(0.0, 1.0); result += mat4(-0.11827391, 0.08306765, -0.3430314, 0.07898041, -0.023839617, -0.019507334, 0.23176382, -0.40992323, 0.09411734, 0.38415068, -0.25845516, -0.29984522, 0.1470966, -0.0684779, -0.07071314, -0.026773235) * go_0(1.0, -1.0); result += mat4(0.19091596, 0.082110435, -0.5266589, -0.1744098, -0.015838385, -0.046316292, 0.023171103, -0.03731331, 0.2642396, 0.31824252, -0.041754793, -0.09525519, -0.14696182, 0.052168854, 0.039857205, -0.027555354) * go_0(1.0, 0.0); result += mat4(0.15207373, 0.09845733, 0.0142631065, 0.096375965, 0.06089903, 0.17902578, -0.42391995, 0.22475442, 0.016356342, -0.06277531, -0.12173141, -0.18635495, -0.0013459618, 0.15725887, 0.019310836, 0.20293565) * go_0(1.0, 1.0); result += mat4(-0.18395247, 0.30672902, 0.09034339, 0.1821889, -0.0419004, -0.2169228, -0.14052129, 0.11006559, 0.1709272, 0.51062274, 0.13758625, -0.2242552, -0.030382963, 0.3357568, -0.26491287, 0.02501938) * go_1(-1.0, -1.0); result += mat4(0.040511727, 0.12523083, -0.27318433, 0.08388512, 0.25354835, 0.3404216, -0.2632471, -0.17784123, 0.2732347, 0.4468553, 0.084667034, -0.1856242, 0.034099877, -0.00954992, -0.32751867, -0.062207516) * go_1(-1.0, 0.0); result += mat4(0.17564747, 0.11645554, -0.16362113, 0.105654195, -0.2762563, -0.1413764, 0.23264363, -0.14000498, 0.095402054, 0.0715738, -0.19346157, -0.028285999, 0.009799127, 0.04059529, 0.19688335, 0.1282381) * go_1(-1.0, 1.0); result += mat4(0.23575781, -0.11446148, -0.20504695, 0.035568226, 0.36890212, -0.85968876, -0.18545328, 0.33796397, -0.30916876, -0.10445518, -0.3046253, 0.33271998, -0.06263589, -0.2160114, -0.16383372, -0.31173357) * go_1(0.0, -1.0); result += mat4(0.20469664, 0.4039374, -0.070057206, 0.030353077, 0.39843914, -0.15490077, -0.24476516, 0.38238233, -0.21809858, 0.23496576, -0.051794037, 0.033664484, -0.14411364, -0.2515329, 0.124655396, -0.05818785) * go_1(0.0, 0.0); result += mat4(-0.09065731, -0.16787091, 0.013269188, 0.23687351, -0.41504318, -0.048163068, 0.31760025, -0.33648986, 0.29752317, 0.2926866, 0.14408836, -0.33382463, -0.15873958, -0.121961035, 0.11797893, 0.09000567) * go_1(0.0, 1.0); result += mat4(0.13356976, 0.013763947, 0.012169505, -0.109594524, 0.03417223, 0.7031121, 0.65146804, 0.5250268, -0.50132495, -0.419648, 0.2940041, 0.83051753, -0.17595838, 0.1633008, -0.018587278, 0.079596795) * go_1(1.0, -1.0); result += mat4(0.07570128, -0.1581438, 0.03904949, 0.14890033, -0.054611947, 0.17469402, -0.44252598, 0.036181703, -0.4981031, -0.37507218, -0.18466389, 0.2645845, 0.25189674, -0.025896115, 0.034307647, -0.020462232) * go_1(1.0, 0.0); result += mat4(-0.11645865, 0.02296537, 0.040909223, 0.015069485, 0.062284566, -0.22526766, 0.09241534, -0.32623053, 0.18208642, 0.3954284, 0.2884468, -0.25137675, -0.037232924, -0.10185309, -0.17956531, 0.018966453) * go_1(1.0, 1.0); result += vec4(-0.16371979, -0.024620198, -0.035754893, 0.04176776); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.01921286, -0.26684764, -0.12663573, 0.31641877, -0.25313398, 0.12264074, 0.58750325, -0.14084283, 0.5837018, -0.042300556, -0.20435576, -0.009954825, 0.060783498, 0.05540401, 0.2205112, -0.06578902) * go_0(-1.0, -1.0); result += mat4(-0.21930243, -0.03774968, 0.22615197, 0.18338196, 0.011201461, -0.271034, 0.00573116, -0.12248194, 0.47990513, 0.2982416, -0.1087603, -0.050099242, -0.07620939, -0.07148229, 0.03691984, -0.16796488) * go_0(-1.0, 0.0); result += mat4(-0.14962853, -0.053769328, 0.02387081, 0.22002189, 0.052237745, -0.26160842, -0.08603077, 0.012542448, 0.08119985, 0.075785555, -0.33437458, -0.43373227, -0.13206963, -0.08759176, -0.03288923, -0.09799959) * go_0(-1.0, 1.0); result += mat4(-0.1305593, -0.5974288, 0.06058367, 0.08406488, 0.013692483, 0.06646377, 0.16469325, 0.08990975, 0.42217395, -0.11289523, -0.06165009, 0.48556912, -0.15702641, -0.19922857, -0.0035429662, -0.0022089656) * go_0(0.0, -1.0); result += mat4(-0.1964807, 0.038099788, 0.21587034, 0.039734077, -0.07063389, 0.11604167, -0.24558097, -0.08900199, -0.7684516, -0.1037487, -0.09380674, 0.33144563, -0.16653742, 0.0028585843, -0.33774406, -0.0528696) * go_0(0.0, 0.0); result += mat4(-0.27298656, -0.05665099, 0.09661685, 0.19780266, 0.1025106, -0.22055034, -0.21218458, -0.040628925, 0.0095010325, 0.13118382, -0.42582452, -0.22197723, 0.21006055, -0.06189587, -0.15285942, -0.09526762) * go_0(0.0, 1.0); result += mat4(-0.14494462, -0.046788953, 0.065877035, 0.09911713, 0.35096622, 0.16682479, 0.028363144, 0.36037162, 0.29413632, 0.28212717, -0.025364442, -0.3406269, 0.047262143, -0.11892685, -0.008032766, 0.29743317) * go_0(1.0, -1.0); result += mat4(-0.15191558, -0.36980554, 0.14555687, 0.0043930537, -0.012661432, 0.15737776, -0.115250416, 0.10324491, 0.24491951, -0.15575431, -0.27802598, 0.21959937, 0.18063772, 0.4455559, -0.09693302, 0.33382267) * go_0(1.0, 0.0); result += mat4(0.2717801, 0.13452889, 0.14105384, 0.16324317, -0.40111846, 0.1154301, -0.0076733204, -0.09697362, 0.44306824, -0.02831414, -0.2153124, -0.12075326, 0.060776163, 0.30347148, -0.0036976219, -0.12070682) * go_0(1.0, 1.0); result += mat4(-0.39780128, -0.29875937, -0.12952097, 0.080333896, 0.07520163, 0.021689568, -0.23121156, -0.038140096, -0.1593877, 0.017156163, -0.06038025, 0.009244022, -0.13917233, 0.30957314, 0.243109, -0.104947075) * go_1(-1.0, -1.0); result += mat4(-0.07965157, 0.06776501, -0.13288979, 0.005851189, -0.08768168, -0.03689969, 0.12034646, 0.22441491, 0.14453568, -0.17648841, -0.3378289, -0.018329712, 0.11722939, -0.34161824, 0.08424494, -0.01400687) * go_1(-1.0, 0.0); result += mat4(0.08153887, 0.07222914, -0.14663404, -0.038526025, -0.07385973, 0.18440577, 0.35890242, 0.17084727, 0.26345527, 0.15280858, -0.007446105, -0.024403179, -0.30336383, -0.22978698, 0.11612946, -0.23614909) * go_1(-1.0, 1.0); result += mat4(-0.07447396, 0.09023449, -0.13798, -0.086943336, -0.30787337, 0.15087669, 0.14418626, -0.03371195, 0.048989657, -0.13075387, -0.13458036, -0.059836224, 0.06495196, 0.269715, 0.3674355, 0.38956037) * go_1(0.0, -1.0); result += mat4(0.34981915, -0.048779126, 0.31717536, 0.38080826, -0.20149232, -0.82969636, -0.10167862, 0.6382858, 0.25976858, 0.4370118, -0.04724865, -0.10014156, 0.19380626, -0.080370255, 0.09578106, -0.035166856) * go_1(0.0, 0.0); result += mat4(-0.026443917, 0.4132611, 0.01822534, 0.12742202, -0.26652107, -0.2996705, 0.30905882, 0.07989903, 0.38249823, 0.21486135, 0.025314959, -0.14717339, -0.13344015, -0.32088286, -0.2833883, -0.30973712) * go_1(0.0, 1.0); result += mat4(0.021517841, 0.006556378, 0.2025686, -0.12044382, -0.38583103, -0.0027515136, -0.06556736, -0.097090125, 0.04676486, -0.11954886, -0.051612873, 0.07831412, -0.18823163, -0.16542958, 0.04245155, 0.6437998) * go_1(1.0, -1.0); result += mat4(-0.39475346, -0.2936861, 0.26768062, -0.28151843, 0.21935691, 0.2101108, -0.15455097, 0.19548604, 0.09188909, -0.020147726, 0.103328265, -0.12574542, -0.34167948, 0.07523185, -0.17669058, 0.62446547) * go_1(1.0, 0.0); result += mat4(-0.37661025, -0.29630858, 0.05451026, 0.1611643, 0.14079669, -0.2170294, -0.038716137, 0.13514164, -0.21235192, -0.07860726, -0.005749412, 0.025625167, -0.13297133, 0.33012658, -0.27434957, -0.18416783) * go_1(1.0, 1.0); result += vec4(-0.0036821906, -0.050239526, -0.01355402, 0.00048220603); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-3x3x3x8 //!HOOK MAIN //!BIND MAIN //!BIND conv2d_2_tf //!SAVE MAIN //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.15873, 0.17989138, 0.14648493, 0.0, -0.017379675, -0.017363746, -0.019855022, 0.0, 0.009670625, 0.0070157526, 0.0075994316, 0.0, 0.025388412, 0.027231036, 0.024052646, 0.0) * go_0(-1.0, -1.0); result += mat4(0.048195973, 0.041760173, 0.037366055, 0.0, -0.115950756, -0.12887983, -0.12535639, 0.0, 0.032125086, 0.03397254, 0.032950625, 0.0, 0.01223746, 0.020822672, 0.0161561, 0.0) * go_0(-1.0, 0.0); result += mat4(0.0890567, 0.094453335, 0.09014035, 0.0, 0.016081346, 0.017434116, 0.020783134, 0.0, -0.011775135, -0.010094134, -0.018522855, 0.0, 0.072103254, 0.07940666, 0.065876864, 0.0) * go_0(-1.0, 1.0); result += mat4(-0.04841196, -0.06963968, -0.056574684, 0.0, 0.10912542, 0.11813441, 0.10643838, 0.0, -0.013013885, -0.01562045, -0.013802797, 0.0, 0.037505716, 0.04352026, 0.04645123, 0.0) * go_0(0.0, -1.0); result += mat4(-0.3472869, -0.36243078, -0.33530185, 0.0, 0.23654196, 0.2305048, 0.22150646, 0.0, -0.045226905, -0.041799217, -0.042511635, 0.0, -0.10267792, -0.1123385, -0.10845448, 0.0) * go_0(0.0, 0.0); result += mat4(0.011987401, 0.012285043, 0.007813165, 0.0, -0.15911353, -0.17523928, -0.1535267, 0.0, 0.15675929, 0.16531634, 0.15948962, 0.0, -0.09240023, -0.09513292, -0.084187366, 0.0) * go_0(0.0, 1.0); result += mat4(0.069052905, 0.07278333, 0.0756627, 0.0, -0.012180326, -0.018794727, -0.031050753, 0.0, -0.044663202, -0.04362803, -0.038904265, 0.0, -0.008540197, -0.011201734, -0.01556625, 0.0) * go_0(1.0, -1.0); result += mat4(-0.08261173, -0.09042543, -0.07589266, 0.0, 0.043515377, 0.045066774, 0.04037769, 0.0, -0.06262993, -0.07469342, -0.058593787, 0.0, 0.026696987, 0.028740842, 0.037405368, 0.0) * go_0(1.0, 0.0); result += mat4(0.07975598, 0.09597654, 0.08997132, 0.0, -0.07844719, -0.07880916, -0.06835411, 0.0, 0.05668995, 0.050163813, 0.053357534, 0.0, -0.020040333, -0.019867316, -0.01907621, 0.0) * go_0(1.0, 1.0); result += mat4(-0.017078733, -0.017393313, -0.008266595, 0.0, -0.0033478448, -0.0027439648, -0.0042334674, 0.0, -0.06354017, -0.062058125, -0.04652064, 0.0, -0.010787706, -0.0062706997, -0.007573461, 0.0) * go_1(-1.0, -1.0); result += mat4(-0.019895451, -0.016341688, -0.008712399, 0.0, 0.026231976, 0.023955572, 0.0216376, 0.0, -0.061950512, -0.05481285, -0.05261985, 0.0, -0.018804235, -0.016235247, -0.0131616965, 0.0) * go_1(-1.0, 0.0); result += mat4(-0.055628926, -0.063315354, -0.057192408, 0.0, -0.0256364, -0.028660972, -0.02937357, 0.0, -0.017604912, -0.020851422, -0.016070362, 0.0, -0.0870202, -0.0832279, -0.07525406, 0.0) * go_1(-1.0, 1.0); result += mat4(0.062738225, 0.07106593, 0.061644047, 0.0, -0.06068257, -0.06983662, -0.066070385, 0.0, 0.024919355, 0.03227179, 0.028569462, 0.0, -0.07866227, -0.098967604, -0.092128105, 0.0) * go_1(0.0, -1.0); result += mat4(0.040397774, 0.047241107, 0.03962998, 0.0, -0.09112752, -0.10057507, -0.09301817, 0.0, 0.10833967, 0.101835825, 0.10027467, 0.0, 0.27189335, 0.27433604, 0.26781923, 0.0) * go_1(0.0, 0.0); result += mat4(-0.044211388, -0.042373534, -0.03658007, 0.0, 0.113148406, 0.12423258, 0.107804194, 0.0, -0.17081551, -0.18562958, -0.17475435, 0.0, 0.09636739, 0.10763415, 0.093332425, 0.0) * go_1(0.0, 1.0); result += mat4(-0.03798545, -0.047811143, -0.050768293, 0.0, 0.018775463, 0.026812987, 0.03452908, 0.0, 0.0055677597, 0.0039081173, -0.0017878668, 0.0, -0.10728597, -0.12618187, -0.109045394, 0.0) * go_1(1.0, -1.0); result += mat4(0.06359783, 0.064184755, 0.04934199, 0.0, -0.009819327, -0.006616115, -0.007431496, 0.0, 0.025055679, 0.024787048, 0.017360551, 0.0, -0.047140837, -0.061695747, -0.06440822, 0.0) * go_1(1.0, 0.0); result += mat4(0.060199022, 0.06482763, 0.059514645, 0.0, 0.026998974, 0.028776823, 0.024897143, 0.0, 0.17968474, 0.19337215, 0.16760105, 0.0, 0.0075838566, 0.010503482, 0.011993149, 0.0) * go_1(1.0, 1.0); result += vec4(-0.0052927984, -0.0060193934, -0.0048643993, 0.0); return result + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Restore_CNN_VL.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(0.1690102, -0.2560719, 0.39658326, -0.3679659, -0.27616683, -0.35619372, -0.3748396, 0.08430813, -0.29574734, -0.31511316, -0.09773105, 0.13616018, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(-0.1326393, -0.259433, 0.025070239, 0.58914864, -0.036478516, 0.30723435, 0.007458902, 0.012962684, 0.2493056, 0.13007334, -0.08448256, -0.38414413, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(-0.11539356, 0.35253766, 0.26143202, 0.2760807, -0.09371543, -0.028165473, -0.028452158, -0.27050856, 0.06718067, -0.0056619495, -0.17654495, 0.17288211, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(-0.16145481, -0.3204927, -0.54317135, 0.11830119, 0.49315026, 0.12008072, 0.50857407, -0.30382085, 0.25807253, 0.020755528, 0.29388228, 0.106109895, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(-0.22728722, 0.50484747, -0.07904469, 0.33114597, 0.50306976, -0.22760947, 0.14773269, 0.17628263, 0.14788547, -0.08223464, -0.10880935, -0.3151985, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(0.3414351, 0.057279214, -0.14419858, 0.09761111, -0.11794496, 0.021717256, -0.22750235, 0.13986664, -0.38932344, 0.28996095, 0.3773904, 0.13175532, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(0.1376552, -0.19587159, -0.35147396, -0.097646296, 0.1686707, -0.14385861, 0.031198, 0.12383533, -0.23089902, 0.08707301, 0.3362293, -0.100579016, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(-0.056774966, 0.047585852, -0.36395878, -0.20211312, 0.4077735, 0.12631284, 0.39813092, -0.033365678, 0.2307249, -0.09131807, 0.20823865, 0.31084216, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(-0.12456089, 0.09755632, 0.31490886, -0.06579996, -0.13386595, 0.07564795, -0.26605195, -0.075180635, -0.11182657, 0.06757017, -0.14351276, -0.16828312, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(-0.046043985, 0.055581126, -0.08791638, -0.13022089); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf1 //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(-0.15485518, -0.29363206, -0.22610365, -0.14291525, -0.45240572, -0.18319772, -0.12209436, 0.15031648, 0.09878383, 0.06711082, 0.25763842, -0.084633484, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(-0.10204406, 0.16167697, 0.22371867, -0.37947702, -0.24476196, -0.038824454, 0.060157117, 0.15764871, -0.08072927, -0.2210841, -0.31835055, 0.009979876, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(0.20506924, 0.21132155, -0.0922578, -0.07430473, 0.14529926, 0.20549752, 0.0077948375, 0.13246094, -0.32353187, 0.21074104, 0.092629515, 0.17590871, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(0.04125819, -0.44050243, 0.23729716, 0.3218237, 0.12943116, -0.011674174, 0.10390632, 0.027775545, -0.20308031, -0.16904089, -0.2121676, -0.022515794, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(0.09664124, 0.20127031, 0.60345304, 0.16697013, 0.23093723, -0.38116834, 0.109695725, 0.0007595324, 0.4092646, 0.009624758, 0.11229678, 0.25326383, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(0.014879592, 0.19204311, 0.07102085, -0.7312604, 0.34860876, 0.3429918, -0.027331594, 0.27636307, 0.1342437, 0.107820466, -0.12645108, 0.21081445, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(-0.12687613, -0.09247973, -0.25973785, 0.4350873, -0.18987224, 0.028678741, -0.0903819, -0.63974863, 0.205591, 0.11308998, 0.18458389, -0.4149041, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(0.34691808, -0.025498383, 0.3428986, 0.21663484, 0.23404741, -0.1725327, -0.0036315925, -0.13299675, -0.1873967, 0.031331502, -0.08785591, -0.0013278709, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(-0.35846514, 0.048703704, -0.104165934, 0.16529736, -0.15378916, 0.26030356, -0.07134151, 0.03692383, -0.15807101, -0.18885155, 0.044707954, -0.11444462, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(-0.0022791293, -0.024132347, -0.57621074, 0.028573977); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.010346764, 0.07230188, -0.24734616, -0.09937907, 0.02228549, -0.19550583, -0.019540425, -0.1037373, 0.033996485, -0.075554, -0.20228972, 0.07090153, -0.09194035, -0.058972966, 0.1768268, 0.27517542) * go_0(-1.0, -1.0); result += mat4(0.020078976, 0.12433655, -0.1620775, 0.036401592, 0.079748705, 0.11660013, 0.17917652, -0.017513236, -0.18936846, 0.24478136, -0.45726213, -0.045004416, -0.08295188, 0.067733586, -0.080548316, 0.2744211) * go_0(-1.0, 0.0); result += mat4(0.024916803, 0.27562472, 0.043771956, -0.012240604, 0.0786355, 0.042651594, 0.16049327, -0.14577515, -0.032735053, 0.17658092, 0.16382934, -0.02337374, 0.11551492, 0.056343183, -0.17930213, 0.14259394) * go_0(-1.0, 1.0); result += mat4(0.20010485, 0.06747722, -0.19026905, 0.11013709, 0.13062745, -0.044626113, -0.0062261797, 0.2189639, 0.1403497, -0.022713251, -0.19452858, -0.010305412, -0.06407589, 0.09836748, 0.025805516, 0.23430973) * go_0(0.0, -1.0); result += mat4(-0.14664203, 0.034910418, 0.024714258, -0.066872925, -0.15717538, -0.14179383, -0.14091893, 0.05859166, 0.18919097, -0.18544437, -0.09068573, -0.08615929, -0.051434122, 0.2170678, 0.18409058, -0.17461225) * go_0(0.0, 0.0); result += mat4(-0.11354446, 0.10745854, 0.2682663, 0.05949201, -0.10695986, 0.1407851, -0.03551388, 0.10691649, -0.17148238, -0.38287184, 0.2074456, 0.11828914, 0.048535194, 0.1464864, -0.18169662, -0.14074169) * go_0(0.0, 1.0); result += mat4(0.22160622, -0.1513045, -0.053284165, 0.033202525, 0.15574448, -0.043640967, -0.0093824165, -0.0019965349, -0.097964935, -0.08289824, 0.08239996, 0.07868361, 0.05731752, -0.20441617, -0.013016076, -0.253108) * go_0(1.0, -1.0); result += mat4(-0.031249097, -0.2272863, 0.23573665, 0.03357689, 0.011395065, -0.10885564, -0.06287508, -0.031719524, 0.10331069, 0.17560169, 0.18303394, 0.022961004, -0.17011635, -0.24371737, 0.10678694, -0.3222825) * go_0(1.0, 0.0); result += mat4(-0.1275465, -0.08844758, 0.10994917, -0.00910273, 0.09393154, 0.03894992, 0.14367905, -0.11811715, -0.09077633, -0.015776094, 0.27427456, -0.13283503, 0.18724327, -0.08139094, 0.04933602, -0.051852766) * go_0(1.0, 1.0); result += mat4(-0.06764611, -0.27426586, 0.12045272, 0.09410856, -0.14258035, 0.11802992, -0.09093882, 0.0022018093, 0.4590643, 0.046258576, -0.07827223, 0.448011, -0.103631735, -0.016930219, -0.15421398, 0.11045997) * go_1(-1.0, -1.0); result += mat4(-0.17295076, 0.00151352, 0.14938255, 0.08336512, -0.07496541, -0.07561223, -0.0846474, 0.14979269, -0.09142163, 0.23925088, -0.015199518, -0.37749895, -0.20636298, -0.022585187, -0.20371509, 0.0745308) * go_1(-1.0, 0.0); result += mat4(0.06458832, -0.009722021, -0.123604394, 0.06548835, -0.3039139, -0.022024399, 0.05297587, -0.0626883, 0.23556642, 0.1516464, -0.07004877, -0.1845364, -0.05918428, 0.19158973, -0.14983447, 0.030489758) * go_1(-1.0, 1.0); result += mat4(0.36604697, 0.17516142, -0.10853731, -0.22694224, -0.107650936, 0.23013335, 0.094055794, -0.17047717, -0.3006048, -0.08621717, -0.18815655, -0.03570218, 0.09676118, -0.017718751, 0.059138596, 0.073388465) * go_1(0.0, -1.0); result += mat4(-0.12791575, 0.101956226, 0.13091874, -0.046373338, 0.04955811, -0.04030444, 0.13869923, -0.046699073, -0.42611042, -0.7173929, 0.052184317, 0.6178025, -0.02929954, -0.07638965, -0.15000828, 0.030710017) * go_1(0.0, 0.0); result += mat4(0.057806686, 0.20842272, -0.20148766, 0.006666912, 0.13356528, -0.45265228, -0.07354092, 0.21447696, 0.019552143, -0.13645506, 0.14643854, -0.0071413796, -0.15487236, -0.002250615, 0.30622452, 0.0033902125) * go_1(0.0, 1.0); result += mat4(0.06896002, 0.24397352, -0.06479052, 0.20676947, -0.24259068, 0.055320013, -0.09032122, -0.11222854, -0.08982342, -0.114818625, -0.06399291, -0.3024516, -0.06302166, -0.1925528, 0.03458982, 0.028828239) * go_1(1.0, -1.0); result += mat4(0.09764086, 0.09599894, -0.0073313303, 0.14418933, -0.045712367, 0.12657364, 0.04620374, -0.069778584, 0.30047333, -0.012418192, 0.15516461, -0.18087754, 0.08178273, 0.14262857, -0.01741533, -0.12509112) * go_1(1.0, 0.0); result += mat4(0.04697884, -0.1506804, 0.031823065, 0.13397239, -0.18396698, 0.10681781, -0.29586303, -0.0039136545, 0.17560847, -0.12486726, -0.018646788, -0.20688744, -0.030614454, -0.0527634, 0.23593572, -0.10542146) * go_1(1.0, 1.0); result += mat4(-0.19182229, -0.32615846, 0.26283535, -0.1371942, -0.071202695, 0.12056063, -0.11450658, -0.27711076, -0.42096004, 0.0014352369, 0.1559669, -0.14464542, -0.17973948, 0.079166576, -0.12501791, -0.20623216) * go_2(-1.0, -1.0); result += mat4(0.12469872, 0.32190827, -0.059510354, 0.1393449, -0.12845798, -0.019571869, -0.22630808, -0.14031963, 0.36072046, 0.05858427, 0.19278921, 0.121090546, -0.067538865, -0.018770566, 0.14318037, -0.15561756) * go_2(-1.0, 0.0); result += mat4(0.024663208, 0.21110268, -0.016415706, 0.060093414, -0.03739678, -0.107412934, -0.077527136, 0.30331334, 0.17196326, -0.15512557, -0.09499732, -0.15748607, -0.16680105, -0.015185634, 0.16114107, -0.21288376) * go_2(-1.0, 1.0); result += mat4(-0.17739037, -0.1190967, 0.13191372, -0.2527187, -0.14992718, -0.30511454, 0.19145966, 0.002194003, -0.12888977, 0.19152176, 0.27528167, 0.099714965, 0.12865707, -0.12051514, -0.055013947, 0.26231763) * go_2(0.0, -1.0); result += mat4(0.46433613, -0.11708138, -0.20157282, 0.32022122, 0.079468675, 0.029407484, 0.2559102, -0.15651533, 0.08644574, -0.09747344, -0.07528584, 0.17354868, 0.19167562, -0.17698488, -0.09896657, 0.17093097) * go_2(0.0, 0.0); result += mat4(0.20283653, -0.33680332, 0.2282385, 0.18832158, 0.20866042, 0.00076752366, 0.16471444, -0.21548858, 0.16193539, 0.17141372, 0.03140222, 0.03913644, -0.030161971, 0.00014570929, 0.08993654, -0.064823024) * go_2(0.0, 1.0); result += mat4(-0.3075755, 0.19942546, 0.015526995, -0.120868504, -0.254515, -0.07791228, 0.03271691, 0.11794217, 0.11258601, 0.045204375, -0.061196107, -0.115958795, 0.3861869, 0.048215542, 0.07016682, -0.009975758) * go_2(1.0, -1.0); result += mat4(-0.07623697, 0.16094944, -0.02283455, 0.14112763, -0.051149167, 0.20429814, 0.011314802, 0.18914083, -0.24240434, -0.08784008, -0.16763984, -0.08492233, 0.31062725, -0.11925119, -0.33195966, 0.2060798) * go_2(1.0, 0.0); result += mat4(-0.016709225, -0.14472668, -0.3677625, -0.09832719, 0.030297454, -0.05775362, -0.1401375, 0.08119674, -0.01795042, 0.05183797, -0.24320887, 0.066842034, -0.22245285, -0.02740993, 0.06316751, 0.053399116) * go_2(1.0, 1.0); result += mat4(-0.039214406, -0.08876633, 0.045552462, 0.19226661, 0.1355001, -0.13942362, 0.17398876, 0.2914014, -0.191809, 0.037143208, 0.013333581, -0.16632195, 0.113767646, -0.106692605, 0.1589787, 0.030107044) * go_3(-1.0, -1.0); result += mat4(0.21997562, 0.13855208, -0.05783191, -0.033682413, -0.010961168, 0.10524961, 0.02177416, 0.18289444, 0.043692037, 0.07853899, -0.039936125, -0.1004449, 0.04494073, -0.020680292, 0.17578089, -0.106598996) * go_3(-1.0, 0.0); result += mat4(0.026852835, -0.16037546, 0.11278316, 0.12656097, -0.006857894, -0.03400118, -0.051564034, 0.00085412664, -0.37556714, -0.05279987, 0.029383834, -0.14246808, -0.056380164, -0.002399925, 0.16025752, 0.036324855) * go_3(-1.0, 1.0); result += mat4(0.022709966, 0.046350412, 0.03390721, 0.02810572, -0.14394265, 0.04215361, -0.3206118, 0.15034916, -0.0028448137, 0.1682989, -0.042686664, 0.020543462, -0.2786501, -0.007482015, -0.040313292, -0.20745736) * go_3(0.0, -1.0); result += mat4(0.05417556, 0.18728684, -0.046121832, -0.27939513, 0.05907976, -0.09191223, -0.16625418, -0.26038164, 0.39956605, -0.052594025, -0.0596556, 0.29517552, -0.015181923, -0.0763375, 0.25131205, 0.13038464) * go_3(0.0, 0.0); result += mat4(-0.036903054, -0.0066989153, -0.062650286, 0.05614359, -0.0064960583, 0.028512698, -0.10906273, -0.010047654, 0.23030473, 0.049983572, 0.10439064, 0.26643834, 0.05041243, 0.09185424, -0.32352915, 0.11295159) * go_3(0.0, 1.0); result += mat4(0.09724027, -0.34962535, 0.06586686, 0.016635379, 0.13831381, 0.01707076, -0.04690347, 0.022350075, 0.018352794, 0.022000022, 0.070613205, 0.117735535, -0.025971051, 0.18832101, -0.09643588, -0.08512127) * go_3(1.0, -1.0); result += mat4(-0.17324433, 0.06810613, -0.057295907, -0.05115964, -0.101570815, 0.12491774, 0.08762367, -0.005862404, -0.05342927, -0.031942457, -0.039624047, -0.04298937, -0.1303138, -0.11869282, -0.024832053, 0.070463404) * go_3(1.0, 0.0); result += mat4(-0.010514842, 0.1376259, -0.11750346, -0.03786737, 0.03459249, 0.015408171, -0.031430878, -0.060825355, -0.072958425, -0.0037895301, 0.041686177, -0.12352204, -0.06261361, 0.054514423, -0.34072715, 0.13860728) * go_3(1.0, 1.0); result += vec4(0.018166734, -0.11002478, -0.05554318, -0.0988193); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf1 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.040142782, 0.0288423, 0.07569487, -0.01490842, 0.14402796, -0.13682005, 0.027765118, 0.03907358, 0.07117706, 0.058157545, -0.23862502, -0.057674367, -0.19220531, 0.0147159435, -0.18028538, 0.0963821) * go_0(-1.0, -1.0); result += mat4(-0.1676744, -0.11937339, 0.12137117, 0.07119485, 0.14148116, -0.043578617, -0.029261118, -0.0016938087, -0.057269357, -0.080076694, 0.12193026, 0.07326153, -0.056278303, -0.01630716, -0.03792076, 0.1483611) * go_0(-1.0, 0.0); result += mat4(-0.3021578, 0.011601693, 0.11266048, 0.19086999, -0.0122412145, 0.08431291, 0.11615175, -0.008039614, -0.39987534, 0.07820729, 0.03509667, 0.1963505, -0.08839513, -0.21571854, 0.059425723, -0.06830175) * go_0(-1.0, 1.0); result += mat4(0.23135209, -0.12452708, 0.0943565, 0.0028859286, -0.09836373, 0.10681712, -0.3535964, 0.08457615, 0.045332734, 0.16579892, -0.03809797, -0.021596594, 0.2937497, -0.028294371, 0.046484597, -0.037604347) * go_0(0.0, -1.0); result += mat4(0.072675414, -0.16431206, 0.28952035, 0.0076831076, -0.020242939, 0.029483542, -0.092415355, 0.08673106, 0.12109694, 0.14307201, 0.23134442, 0.11731775, 0.09981601, -0.16968462, 0.037470713, 0.14948717) * go_0(0.0, 0.0); result += mat4(0.0029752052, 0.06526503, 0.1866458, 0.07451277, -0.31836876, 0.17115082, -0.13969697, 0.23844297, -0.03244903, -0.08832665, 0.023691226, -0.18230624, -0.074933805, -0.00044301842, 0.050572682, 0.081511915) * go_0(0.0, 1.0); result += mat4(0.039502528, 0.051221415, -0.13968123, -0.091212444, -0.016925618, 0.15409444, -0.017455677, -0.11653652, 0.03539446, -0.00087720866, -0.12839639, 0.037198763, 0.03674469, -0.26444665, 0.019721227, -0.13013805) * go_0(1.0, -1.0); result += mat4(0.039229527, 0.25667152, 0.0032586441, -0.00718359, 0.1617932, 0.10409968, 0.07182867, -0.09810605, 0.07789241, -0.02014911, 0.025767172, -0.14604759, 0.07175764, 0.32513744, -0.20473222, -0.16266066) * go_0(1.0, 0.0); result += mat4(0.13418433, 0.061813723, -0.13927278, -0.2498272, 0.03468218, 0.29483125, 0.063289374, -0.04726235, 0.1898295, -0.33132064, 0.032045014, 0.02159535, -0.1148363, 0.31306976, 0.06456038, 0.048988886) * go_0(1.0, 1.0); result += mat4(0.07151646, 0.2799246, -0.107190795, -0.16431166, -0.28007045, 0.07206954, 0.06775463, 0.009758042, 0.07032184, -0.20843789, 0.087045245, 0.1360676, -0.25718534, 0.028249472, -0.12614648, 0.009949602) * go_1(-1.0, -1.0); result += mat4(0.020241471, -0.23390484, -0.0083223935, 0.08344701, 0.08222297, 0.12026539, -0.08652223, -0.08228822, -0.039576706, -0.24677879, -0.1157289, 0.2590508, -0.23809408, 0.19911982, -0.116798095, -0.035870325) * go_1(-1.0, 0.0); result += mat4(0.024991842, 0.050509237, -0.024134455, -0.12659028, 0.24089767, 0.122712664, -0.10482493, -0.19403952, -0.19177693, -0.06538376, -0.041478425, 0.32176673, -0.1534002, -0.18680622, 0.06763643, 0.020806564) * go_1(-1.0, 1.0); result += mat4(0.03437814, -0.28067374, 0.2830681, 0.038812317, -0.021698112, -0.120865285, 0.22695538, -0.045419116, -0.030475847, -0.01977341, -0.1265364, -0.3109814, 0.012255813, 0.053917278, -0.018620957, -0.14599285) * go_1(0.0, -1.0); result += mat4(-0.016204128, -0.04093018, 0.054571863, 0.02679643, 0.01756274, -0.057685968, 0.16148666, 0.17370272, -0.11065411, 0.06378157, -0.09331551, 0.22985275, 0.057905316, 0.12323568, 0.07748665, 0.09878629) * go_1(0.0, 0.0); result += mat4(-0.018112244, 0.063234635, -0.013184602, 0.16241394, 0.08877139, 0.02145378, -0.02490027, -0.038920373, 0.13127136, 0.14391647, 0.020553736, 0.14401346, 0.06685973, -0.25398204, 0.10369067, -0.055949755) * go_1(0.0, 1.0); result += mat4(0.07710333, 0.047412727, 0.13813803, 0.18624061, 0.16907091, -0.039532468, 0.06234584, 0.06408178, -0.054543987, -0.045220226, -0.11093376, -0.37399602, 0.20372874, 0.004580967, -0.07742308, 0.017989937) * go_1(1.0, -1.0); result += mat4(0.003485311, -0.08897399, -0.013108594, -0.19473282, -0.27081844, -0.16812073, 0.0052992934, -0.055331517, 0.09446357, 0.019280333, 0.16560757, -0.3230032, 0.043096773, 0.059222896, -0.064184934, -0.059852477) * go_1(1.0, 0.0); result += mat4(0.06794279, -0.034135245, 0.083064295, 0.13506731, 0.13064219, -0.44978833, -0.03513717, 0.08999715, 0.1124541, 0.42208397, -0.0038724816, -0.014332087, -0.13751853, -0.04929869, 0.09134992, -0.17687531) * go_1(1.0, 1.0); result += mat4(0.100909084, -0.0131197255, 0.082274795, -0.2138443, -0.08515947, -0.021058358, 0.10951775, -0.06349191, -0.29129833, -0.029262653, 0.25235432, -0.11748315, 0.121980384, 0.062347785, 0.10916932, -0.15993518) * go_2(-1.0, -1.0); result += mat4(0.28893283, -0.05677308, -0.2641288, -0.058937225, -0.16187571, 0.006647366, -0.063294955, 0.04766719, 0.60601914, -0.07831864, -0.15710756, -0.011491797, 0.15587467, -0.08105375, 0.07847514, -0.2803333) * go_2(-1.0, 0.0); result += mat4(-0.077989794, -0.09871811, -0.3516344, 0.15292728, 0.010889273, 0.0011189661, -0.16118282, -0.018821161, -0.039708678, -0.00060983415, -0.06367813, 0.009148068, 0.03919827, 0.18782744, 0.028040757, -0.10230145) * go_2(-1.0, 1.0); result += mat4(-0.4079609, 0.18640275, -0.12475227, 0.13891742, 0.25121725, 0.16942379, 0.14409852, 0.087600805, 0.045335658, -0.12683709, -0.0077387216, 0.06563413, -0.19857128, 0.106910795, -0.048285246, 0.10768945) * go_2(0.0, -1.0); result += mat4(0.5989075, 0.20941062, -0.20086494, 0.13344856, 0.073034994, 0.22358665, 0.101664364, -0.13463663, 0.18816395, -0.061176624, -0.14712185, 0.027320342, -0.09529667, 0.031148786, -0.28744993, 0.18698911) * go_2(0.0, 0.0); result += mat4(0.14799193, 0.39471942, -0.23340325, -0.4031061, 0.18926248, -0.11091216, 0.118981816, -0.09155061, 0.17049436, 0.19803695, -0.1513267, 0.023817873, 0.0090933135, -0.04134864, 0.060486555, 0.03536634) * go_2(0.0, 1.0); result += mat4(-0.39094314, 0.01779997, 0.12710269, 0.0067333193, -0.31255835, -0.08206612, -0.048528638, 0.369439, -0.19351655, -0.03420455, 0.15831526, -0.052294146, -0.08481741, 0.0787108, 0.1312136, -0.108919285) * go_2(1.0, -1.0); result += mat4(-0.16068119, -0.42190582, 0.19383872, -0.018445708, 0.09803051, -0.020769652, -0.022599563, -0.052448895, -0.20645833, -0.031432863, 0.0025441595, 0.03410379, -0.20268854, 0.04481527, 0.05191063, 0.42317194) * go_2(1.0, 0.0); result += mat4(-0.12786235, -0.23936178, 0.116561726, 0.30756372, -0.09420156, -0.044529166, -0.03585749, 0.1829332, -0.23939075, 0.24030831, 0.019878127, -0.015069802, 0.24300557, -0.22558568, -0.104956664, -0.09393648) * go_2(1.0, 1.0); result += mat4(-0.04607054, 0.012677649, -0.027597688, 0.1618836, 0.29210827, 0.014221155, -0.13591036, -0.06895336, -0.09559534, 0.07956421, -0.11112994, -0.13325493, 0.24562472, 0.11046177, 0.057847694, 0.0016315983) * go_3(-1.0, -1.0); result += mat4(-0.03365951, 0.027391057, 0.09653403, -0.14718771, -0.049631152, -0.06467214, -0.058545876, 0.1424002, -0.06320376, 0.181183, 0.10249362, -0.16052136, 0.3013475, -0.04156266, 0.08862033, 0.06888033) * go_3(-1.0, 0.0); result += mat4(0.10045977, -0.004198456, -0.025856055, 0.05739418, -0.1328637, -0.025975171, 0.06553717, 0.11301186, 0.0704087, -0.083569765, 0.16066101, -0.24453588, 0.25370175, 0.037184533, 0.062386766, -0.20025635) * go_3(-1.0, 1.0); result += mat4(-0.017958941, 0.06417776, -0.1525265, 0.12451173, 0.14567685, -0.0049682115, -0.23973411, -0.0783304, -0.010629432, 0.08055161, 0.2028341, 0.17640644, -0.20445108, -0.055524793, -0.019326134, 0.081288636) * go_3(0.0, -1.0); result += mat4(0.007882519, -0.03722546, 0.053249408, 0.00071846246, -0.07053029, -0.21583866, 0.1415364, -0.19486657, 0.20685542, 0.17660026, -0.32156837, 0.1746825, -0.14957622, -0.09224378, -0.098153435, -0.13054638) * go_3(0.0, 0.0); result += mat4(0.10051427, -0.17398237, 0.09842799, -0.14187703, 0.116901085, -0.1229543, -0.0007776771, -0.20410055, -0.11373484, -0.111150615, -0.1974002, -0.11641459, 0.024105398, 0.24985977, 0.015871854, -0.10724633) * go_3(0.0, 1.0); result += mat4(-0.18081793, 0.1209351, -0.12867971, -0.019415248, 0.062617876, -0.037130393, -0.07803658, -0.22862352, 0.2586428, -0.030090366, -0.11894069, 0.18087515, -0.40921417, 0.070013195, 0.030540073, 0.035120826) * go_3(1.0, -1.0); result += mat4(-0.13185939, 0.12992652, 0.08125049, 0.075331174, 0.064219765, 0.056629725, -0.020012032, -0.0855444, -0.044063166, -0.05396545, -0.028002812, 0.21837157, -0.15206428, -0.12681007, 0.14895032, 0.12339962) * go_3(1.0, 0.0); result += mat4(0.08066341, -0.14773634, -0.0212227, -0.014011867, -0.048505764, 0.075407125, -0.020620076, 0.0003291325, -0.21815202, -0.23136546, 0.10853532, -0.036058456, 0.10952532, -0.052677035, -0.13005799, 0.18398996) * go_3(1.0, 1.0); result += vec4(0.022609137, -0.028548084, 0.024431901, 0.010504478); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.069641694, 0.104958326, 0.14786446, 0.027633663, -0.004279524, -0.020451711, 0.0883571, -0.016224537, 0.13585235, 0.11078269, 0.20198658, -0.042161036, 0.020466218, 0.20994963, 0.20072585, -0.028024657) * go_0(-1.0, -1.0); result += mat4(0.050872434, 0.12874635, 0.1298729, 0.115810685, 0.07087254, 0.09885682, 0.23018982, 0.19187538, 0.10953604, 0.0033836907, -0.13325337, 0.09830315, -0.06528767, 0.05096927, -0.016355392, -0.039334368) * go_0(-1.0, 0.0); result += mat4(0.027010268, 0.018263958, 0.0360758, 0.016791478, 0.2815702, 0.15517488, 0.43415815, 0.044976447, -0.0070842914, -0.12546758, 0.16874593, 0.077622116, 0.02252915, 0.1769774, 0.07181055, -0.15128697) * go_0(-1.0, 1.0); result += mat4(0.057129618, 0.118046716, 0.07237424, -0.07842637, -0.044214778, -0.12886304, 0.08603301, -0.10416606, -0.15852053, 0.3788151, 0.26181692, -0.09092249, 0.31635332, 0.064212754, 0.21923725, 0.07500004) * go_0(0.0, -1.0); result += mat4(-0.16981383, 0.044409662, -0.3717617, -0.031610407, 0.03658662, -0.09459229, -0.09449437, -0.014000666, -0.19656453, 0.03934163, -0.16304104, -0.12761801, -0.06235523, 0.16438273, -0.036933117, -0.095564745) * go_0(0.0, 0.0); result += mat4(0.09725091, 0.034022827, 0.17699842, 0.1079676, -0.13236652, 0.03718181, -0.06968635, -0.23288171, 0.10275666, 0.08464966, -0.37162134, -0.35782215, -0.11023659, 0.2519236, -0.035197742, -0.019324787) * go_0(0.0, 1.0); result += mat4(-0.09968464, 0.01102193, 0.0073735216, 0.011999313, -0.004998707, 0.09518938, 0.045727003, -0.21544908, 0.006879454, -0.06398254, -0.12584935, -0.06759933, -0.0820037, -0.07775104, 0.021957919, -0.122708224) * go_0(1.0, -1.0); result += mat4(-0.08869767, 0.031296413, -0.0034280645, 0.13778855, 0.10073061, -0.08393937, -0.032959275, -0.0500518, 0.010908757, -0.09189417, -0.057760105, 0.17652664, -0.08729078, -0.09639096, -0.25654703, 0.055152636) * go_0(1.0, 0.0); result += mat4(0.0027847723, -0.12885433, 0.038065907, 0.17450769, 0.0864409, 0.04592345, -0.015443841, 0.077010944, 0.08967368, 0.06800111, -0.23636387, 0.35023567, 0.03165923, 0.03132063, 0.17964344, 0.035610788) * go_0(1.0, 1.0); result += mat4(-0.032017227, -0.0022808525, -0.08470573, 0.05332408, -0.14674746, 0.025374275, -0.018281924, 0.041163016, 0.00096549373, 0.014724006, 0.004913065, 0.18494442, 0.034953076, -0.15731992, -0.13792977, 0.08041999) * go_1(-1.0, -1.0); result += mat4(0.08305006, 8.6318905e-05, -0.007895379, 0.02731387, -0.061324496, 0.050034665, 0.22662131, -0.013876427, -0.074468784, -0.008136604, -0.23337875, -0.1742574, 0.011753501, -0.11666686, -0.22541048, -0.14549944) * go_1(-1.0, 0.0); result += mat4(-0.028333234, 0.121047184, 0.06720256, -0.058930036, 0.030258363, 0.07292774, 0.06455556, 0.0019076486, 0.0073987027, 0.17144889, 0.06084024, -0.08762086, -0.114422195, -0.16595861, -0.08706028, -0.10736261) * go_1(-1.0, 1.0); result += mat4(-0.02519315, -0.14611271, 0.0388848, 0.19481422, -0.05970354, -0.08391417, 0.18982239, -0.10447052, 0.15587378, -0.023997072, 0.0781739, 0.2182389, -0.023886079, -0.1422596, -0.13352804, 0.005008043) * go_1(0.0, -1.0); result += mat4(0.08842712, -0.100292705, 0.18925671, 0.12198875, 0.061771665, -0.04473232, 0.025053164, 0.039047796, -0.1672479, -0.08934517, 0.33099812, -0.20269585, -0.21640155, -0.22029749, 0.16539703, -0.2442679) * go_1(0.0, 0.0); result += mat4(-0.16332205, -0.101898365, 0.02919932, -0.11900455, 0.14442924, 0.0916815, 0.037550304, 0.024123482, 0.02042624, 0.033472955, -0.059437107, -0.18735693, -0.013749093, -0.06199881, -0.08685079, 0.04252364) * go_1(0.0, 1.0); result += mat4(-0.09047013, -0.055188328, -0.09106191, -0.048969727, 0.05114009, -0.12753403, 0.07116141, 0.060749624, -0.074034564, -0.21952136, -0.09479503, 0.2753584, -0.014141759, -0.14883812, -0.0673838, -0.012279045) * go_1(1.0, -1.0); result += mat4(0.013816464, -0.0747162, -0.19202435, -0.064403646, 0.34980014, 0.04375546, 0.20264609, 0.006684355, 0.11523799, 0.024674915, -0.08697566, -0.04662527, -0.12743855, -0.39463726, 0.0057380227, 0.01286557) * go_1(1.0, 0.0); result += mat4(-0.08146522, 0.074080914, -0.16856177, -0.183158, 0.19228102, 0.12373886, 0.017574452, -0.01753772, 0.045071773, 0.07725093, 0.023422163, -0.011545186, 0.20751388, -0.10795588, 0.07606346, 0.10282933) * go_1(1.0, 1.0); result += mat4(0.12512013, -0.102208994, -0.09125398, 0.12043188, -0.066011876, 0.08831903, -0.017038671, -0.005541508, -0.049607087, 0.08654939, -0.02037085, 0.26887566, 0.005012545, 0.01869507, -0.013064982, -0.010649147) * go_2(-1.0, -1.0); result += mat4(0.006824864, -0.05071593, -0.20786697, -0.07327317, 0.011382597, 0.030494886, -0.04754353, -0.018284699, 0.01305972, -0.036589053, 0.26637617, 0.021887446, -0.026669119, -0.037982125, -0.063445956, -0.009104248) * go_2(-1.0, 0.0); result += mat4(0.032602567, 0.07094331, 0.052653246, 0.08342047, -0.085082285, -0.14674088, -0.23073354, -0.07915851, 0.0017120204, 0.032407638, -0.039819505, 0.16942178, 0.023192152, -0.0353237, 0.10930186, 0.22939779) * go_2(-1.0, 1.0); result += mat4(0.0010455973, -0.11821993, -0.12639599, 0.12250084, -0.12756817, 0.11478416, -0.1862587, 0.016819192, 0.02110181, -0.25492984, -0.1766048, 0.22188173, -0.21305011, 0.113442205, 0.04599144, -0.15840286) * go_2(0.0, -1.0); result += mat4(-0.15086032, -0.17428935, 0.39080557, 0.07576757, 0.121703945, 0.17944208, -0.003140103, -0.11231332, 0.12102969, 0.15310267, 0.17578171, 0.40631834, -0.21299168, 0.024928993, 0.030104794, 0.020753227) * go_2(0.0, 0.0); result += mat4(-0.098734386, -0.020072265, -0.14308836, -0.08490801, 0.017175158, 0.02250534, 0.04060829, 0.033022214, 0.0046218676, 0.17923212, 0.0112105915, 0.09574084, 0.14819936, -0.14692923, 0.12634254, 0.060762513) * go_2(0.0, 1.0); result += mat4(0.030521613, -0.097913325, -0.016720278, 0.11273997, 0.013019863, -0.06557118, 0.0405774, 0.0915019, 0.022414956, -0.053254984, 0.18639986, 0.07820968, 0.06498986, 0.058922634, -0.02240318, -0.086019725) * go_2(1.0, -1.0); result += mat4(0.2058775, 0.01502064, 0.05847032, 0.007249146, 0.086483665, 0.19420148, 0.03892261, -0.013546935, -0.07980237, 0.04347281, -0.10376214, -0.1366535, 0.05285337, 0.07213318, 0.3642818, -0.11331124) * go_2(1.0, 0.0); result += mat4(-0.025740806, 0.14551482, -0.037410017, -0.17477523, -0.11853099, -0.060820814, -0.102599286, -0.13267937, -0.103053465, -0.014044828, -0.01888072, -0.06499249, 0.22311528, -0.051850274, -0.034120858, 0.044562567) * go_2(1.0, 1.0); result += mat4(-0.21360217, 0.10093803, -0.0016407765, -0.1473997, 0.26524043, 0.02112132, 0.23173104, -0.013157391, 0.05945182, 0.044635538, 0.06031638, -0.21435826, -0.10147484, 0.069090195, 0.09641844, -0.09581093) * go_3(-1.0, -1.0); result += mat4(-0.08576515, -0.122861005, 0.049567085, -0.085854456, 0.23809357, -0.024966082, -0.10294079, 0.046241313, 0.008621132, -0.08323767, 0.20277941, 0.163423, -0.07386535, -0.088738985, 0.05274358, -0.025479877) * go_3(-1.0, 0.0); result += mat4(-0.041135542, -0.008365642, 0.17088248, 0.04025207, 0.13809255, -0.056895368, -0.01582834, 0.07361908, -0.00068995473, -0.09300962, 0.19117641, 0.24832036, -0.06572358, -0.026025, -0.019093119, -0.049720034) * go_3(-1.0, 1.0); result += mat4(0.024900286, 0.11525501, 0.025882801, 0.037742402, 0.36976853, 0.052211333, -0.15143296, 0.1802276, -0.059080046, 0.017990451, 0.026395092, -0.12689115, -0.07705386, 0.1232379, 0.13273561, -0.12521964) * go_3(0.0, -1.0); result += mat4(-0.19788785, 0.044887315, 0.07663442, 0.16688696, -0.2842248, -0.15684547, 0.028387763, 0.0063470444, -0.012245601, -0.038382255, -0.8187406, -0.25245667, 0.23014604, 0.22746666, 0.1594356, 0.16469443) * go_3(0.0, 0.0); result += mat4(-0.12663333, 0.014730006, 0.03765697, 0.15704912, -0.106595434, -0.05317512, -0.081759915, -0.08797109, 0.064620756, -0.06341419, 0.16493447, 0.23102313, 0.068325415, -0.088058695, 0.16885915, 0.036382258) * go_3(0.0, 1.0); result += mat4(0.035389822, -0.11811836, -0.035656307, -0.0680554, 0.1338908, 0.065852076, 0.023307983, 0.0675308, 0.09690683, 0.18170924, 0.09862692, -0.20964378, -0.08601271, -0.20016764, -0.01879598, -0.14629345) * go_3(1.0, -1.0); result += mat4(-0.27183273, 0.013525998, -0.14995874, -0.23938845, -0.26218823, -0.0009874097, -0.13385512, -0.10664239, -0.048931994, 0.039898522, 0.047444753, 0.10934722, 0.10969629, 0.123539805, 0.11692802, 0.14172275) * go_3(1.0, 0.0); result += mat4(-0.1656506, 0.019683002, 0.0221048, 0.12596753, 0.20420644, -0.07930122, 0.04653823, 0.11492255, -0.0050175437, -0.03271697, 0.013389486, 0.034583613, -0.2196601, -0.1615663, -0.013763388, -0.056037936) * go_3(1.0, 1.0); result += vec4(-0.022956269, 0.029688787, -0.070148066, -0.07163476); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf1 //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.15104648, 0.05522861, -0.0654341, -0.053517453, -0.08264124, -0.0062249107, -0.20364265, -0.05015117, -0.18837251, 0.030655831, 0.046844713, -0.20673253, -0.14042036, -0.05655449, 0.13994302, 0.011745607) * go_0(-1.0, -1.0); result += mat4(-0.16517559, 0.1489214, -0.09149559, 0.025003506, -0.124926426, 0.16974348, -0.020857265, 0.08017403, 0.21836148, 0.0025619378, 0.2331612, 0.085599184, -0.030934382, -0.055194855, 0.09527726, -0.10081552) * go_0(-1.0, 0.0); result += mat4(0.041800212, 0.028859638, 0.09395546, 0.05211183, -0.038541477, 0.021495212, 0.04862346, -0.007864793, 0.038407274, -0.13841268, -0.14963801, 0.26470762, 0.16691841, -0.07262008, 0.034374326, -0.14709206) * go_0(-1.0, 1.0); result += mat4(0.00094978884, -0.028974704, -0.0900548, -0.08401967, -0.08935931, -0.043606587, -0.14497143, -0.05226239, -0.21516493, 0.19410603, -0.089924194, -0.04335071, -0.012618276, -0.2671613, 0.020422975, -0.037739716) * go_0(0.0, -1.0); result += mat4(-0.13403237, -0.02524383, -0.03474901, 0.054432765, 0.11946775, 0.107336655, -0.1431715, -0.13370377, 0.015087512, -0.1917613, 0.073493585, 0.2788855, -0.010510839, 0.06891479, -0.06741307, -0.05271205) * go_0(0.0, 0.0); result += mat4(-0.15432046, 0.04021662, -0.16979513, 0.13660534, -0.10518303, -0.10095502, -0.13092068, 0.022805348, -0.16676381, -0.4273298, 0.020867536, 0.3506733, -0.29459694, -0.055828743, -0.069241956, 0.04106382) * go_0(0.0, 1.0); result += mat4(-0.08890133, 0.07549666, -0.040735144, -0.1506932, -0.22227979, -0.0762723, -0.17766447, -0.05741318, -0.21885683, 0.2379157, -0.15525854, -0.07306285, 0.15580738, -0.04394069, -0.19175608, 0.018283797) * go_0(1.0, -1.0); result += mat4(-0.08503275, -0.105500385, -0.114987396, -0.07166016, -0.2147138, 0.09378708, 0.24550334, -0.0834075, -0.033147786, -0.022304727, -0.31062204, 0.027651973, 0.109098755, 0.18889032, 0.1163026, 0.13863255) * go_0(1.0, 0.0); result += mat4(0.15266588, -0.14901319, 0.033916786, 0.09381096, -0.08196443, -0.16194504, 0.035789456, 0.21234898, -0.48724765, 0.2619442, -0.11215393, 0.25061038, 0.022344576, 0.0116525125, 0.111661114, -0.15242295) * go_0(1.0, 1.0); result += mat4(0.020475458, 0.0797404, -0.13576819, 0.009681671, 0.030504882, 0.049232908, 0.022025917, 0.16912088, -0.23914136, -0.084663324, 0.020925451, -0.1023938, 0.035916872, -0.07538111, -0.11470242, 0.15238516) * go_1(-1.0, -1.0); result += mat4(-0.12941381, 0.08509899, -0.029489802, -0.09148447, -0.089406274, -0.116145454, -0.08979843, 0.11908148, 0.15473351, -0.21687616, 0.12607013, -0.08244334, -0.079580925, -0.16613089, -0.09287793, -0.03412643) * go_1(-1.0, 0.0); result += mat4(-0.023578499, 0.07394217, -0.13069086, -0.1060499, -0.07559958, -0.21839201, 0.1090753, 0.0787872, 0.07677037, -0.25998843, 0.20039314, 0.046882212, 0.31871012, -0.3048051, 0.15118991, -0.00518087) * go_1(-1.0, 1.0); result += mat4(-0.15338503, -0.11057532, 0.075839415, -0.18592294, -0.0155324, 0.038140323, -0.10498194, 0.09070477, 0.05108992, -0.047939524, -0.091004305, 0.09649005, -0.10967152, -0.051909525, -0.05314551, 0.09661584) * go_1(0.0, -1.0); result += mat4(-0.14458802, -0.053263694, -0.0010885567, 0.23342133, 0.01918937, 0.12026143, -0.15691495, 0.30480555, -0.08725869, 0.19082253, 0.3594973, 0.016653897, 0.045152336, -0.088590585, 0.0069655925, 0.1392425) * go_1(0.0, 0.0); result += mat4(0.17944881, -0.17950764, 0.13282645, 0.030974053, 0.32233685, 0.18067117, -0.11472813, 0.097301506, -0.047649745, -0.1053861, -0.081039384, 0.035132434, 0.10204545, 0.085582554, -0.13153993, -0.021741152) * go_1(0.0, 1.0); result += mat4(-0.15573682, 0.16409989, -0.22574787, -0.03877603, -0.18285516, 0.11638645, 0.18321282, -0.017770218, 0.18230622, 0.16433364, -0.12795393, -0.03805153, 0.14386104, -0.0891527, -0.056928284, -0.10961495) * go_1(1.0, -1.0); result += mat4(0.257622, 0.052519716, -0.25421762, -0.1887382, -0.083568096, -0.0064690276, -0.029110614, 0.103327505, -0.17006217, 0.2254096, -0.29366904, 0.04302887, -0.10198446, -0.24423616, 0.16781262, -0.005019004) * go_1(1.0, 0.0); result += mat4(0.103393994, -0.059044626, -0.18192382, 0.0990813, -0.26143607, 0.11036474, 0.04788275, -0.096738026, 0.12825653, 0.13631694, -0.077904984, -0.020790676, -0.25118098, 0.122588515, -0.049440473, -0.10758222) * go_1(1.0, 1.0); result += mat4(0.06693113, -0.13647175, 0.131139, 0.13143918, 0.081720434, 0.117537096, 0.15387627, -0.008771362, 0.08513583, 0.023794742, -0.0661625, 0.115793936, 0.0023350024, 0.02215075, -0.0494433, -0.013404977) * go_2(-1.0, -1.0); result += mat4(0.041419264, -0.17622781, 0.028418267, 0.12114493, -0.23587078, 0.08457395, 0.014364018, -0.103271864, -0.051572207, -0.026424447, 0.16755055, -0.10763651, -0.033440586, 0.068594255, -0.050668504, 0.1941505) * go_2(-1.0, 0.0); result += mat4(-0.2780181, 0.037816502, -0.11516711, -0.09822884, 0.13762361, -0.14317706, 0.14350282, 0.000623895, -0.08601606, 0.08118504, 0.15497385, -0.04721711, -0.008936935, -0.014223618, -0.09641698, -0.013884213) * go_2(-1.0, 1.0); result += mat4(0.14349665, -0.03144472, -0.057813704, 0.0667044, 0.09026094, 0.051366236, 0.11139983, -0.015782114, -0.18314016, -0.18774192, 0.0014838242, 0.15759028, 0.062388215, 0.13626057, 0.02576217, -0.06317815) * go_2(0.0, -1.0); result += mat4(0.07151769, 0.14508991, 0.1736844, -0.11487795, -0.07999805, -0.07797908, 0.037923355, -0.059138823, -0.23531209, -0.040207293, -0.068355694, -0.024296658, -0.114820175, 0.19726487, 0.21772414, 0.03659222) * go_2(0.0, 0.0); result += mat4(0.16858695, -0.12135113, 0.009391182, -0.081519485, 0.13340487, 0.07007004, 0.094124354, 0.035519842, -0.3320139, -0.06624027, -0.14716229, -0.09205287, 0.12664132, -0.05655441, 0.0123263765, 0.04641279) * go_2(0.0, 1.0); result += mat4(0.19018422, -0.15428329, -0.009354114, 0.04165953, 0.11024837, -0.107493006, -0.05807292, -0.048029456, 0.24319384, -0.10542357, -0.013699952, 0.06228662, -0.06808749, -0.023227982, 0.16528323, -0.05610251) * go_2(1.0, -1.0); result += mat4(-0.008616222, 0.077674195, -0.08638503, 0.09293109, 0.072474636, 0.05004233, -0.20591061, -0.005301386, -0.15486047, 0.15038474, 0.1262478, 0.021724822, 0.02274613, -0.3088281, -0.08437887, -0.10684698) * go_2(1.0, 0.0); result += mat4(-0.16960032, 0.09365251, -0.030414175, -0.010766254, 0.18181023, 0.12130318, 0.08913089, -0.06070321, 0.05200306, 0.092584535, 0.17694671, 0.033796314, -0.038107123, -0.04335955, -0.049443472, 0.30465958) * go_2(1.0, 1.0); result += mat4(0.07661484, -0.009945252, 0.12866217, -0.07592757, -0.21030053, 0.014371748, -0.072458774, -0.04700072, 0.15534303, 0.2007125, -0.15699059, -0.032897495, 0.08110436, -0.11243608, 0.008632577, -0.10153441) * go_3(-1.0, -1.0); result += mat4(-0.034697928, 0.06928288, -0.2796273, 0.14405379, 0.12248569, 0.036539096, 0.06607706, 0.077684596, -0.16473202, 0.1665916, -0.29977503, 0.21047153, 0.13114224, -0.091579035, -0.045458574, 0.03254245) * go_3(-1.0, 0.0); result += mat4(0.053284872, 0.053366095, -0.26152626, -0.03123967, -0.031794485, 0.17670582, -0.07450994, 0.017521491, -0.040290453, 0.38342363, -0.25021288, -0.014660264, 0.1621895, 0.25041878, -0.12124821, 0.068036206) * go_3(-1.0, 1.0); result += mat4(0.11366693, -0.030863572, -0.07411263, 0.12475283, -0.046070684, -0.09033321, 0.013222701, 0.06798592, -0.32814804, 0.057653826, -0.14082801, -0.00217398, -0.22856179, -0.19058353, -0.20992154, -0.03701372) * go_3(0.0, -1.0); result += mat4(0.20345633, -0.1332355, 0.27152926, -0.13477845, -0.25242096, -0.28281286, 0.31289554, 0.14284514, 0.53362453, -0.46766588, 0.4518293, -0.39291728, -0.3573227, -0.014670052, 0.0051881406, 0.16552156) * go_3(0.0, 0.0); result += mat4(-0.15017267, -0.07792945, -0.204405, 0.13964304, -0.13642666, -0.10228306, 0.03238279, -0.08689329, -0.072262034, -0.0258388, 0.05689183, 0.055701543, -0.19800112, 0.012217054, -0.033292748, -0.047611095) * go_3(0.0, 1.0); result += mat4(-0.014704416, -0.12203891, 0.066083655, -0.1409769, 0.0041513643, -0.087383606, -0.17498164, 0.11327789, -0.25947225, -0.0016027623, 0.08202566, 0.042270098, 0.006429511, -0.26576808, -0.08461341, 0.049376782) * go_3(1.0, -1.0); result += mat4(0.0695189, -0.14753938, 0.09578246, -0.16607563, -0.0105561055, 0.17166016, 0.027422488, -0.14175262, -0.009492696, -0.23449713, 0.018270867, 0.14635146, 0.33451268, 0.030959005, -0.46468422, 0.024256868) * go_3(1.0, 0.0); result += mat4(-0.16865666, -0.00015881563, -0.054488145, -0.06222717, -0.032101758, 0.06485387, -0.0028512608, 0.046645947, 0.017593225, -0.19447896, -0.024742266, 0.03970127, 0.29845607, -0.16168733, 0.035172883, 0.07924657) * go_3(1.0, 1.0); result += vec4(0.103826486, 0.045373913, 0.11565896, -0.06568643); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.1851775, 0.053705044, 0.033816848, -0.018555025, -0.21204336, -0.01706974, 0.088259794, -0.13126148, 0.10729598, -0.043457437, 0.08634712, 0.09220895, 0.062131613, -0.01995871, 0.05181067, 0.18520063) * go_0(-1.0, -1.0); result += mat4(0.1662002, -0.14197104, -0.052809287, 0.025287712, -0.08330898, -0.08998097, -0.15642618, -0.14941245, -0.03481203, 0.061857622, 0.26051775, -0.0005498248, 0.086427025, 0.024108192, -0.12418039, 0.022286376) * go_0(-1.0, 0.0); result += mat4(0.058200672, -0.3073398, 0.17150162, -0.13394679, -0.075118184, -0.14607768, -0.006172172, 0.007731589, -0.21818224, -0.06449433, -0.038958784, 0.037722416, 0.28699976, -0.027563032, 0.23295315, 0.028444216) * go_0(-1.0, 1.0); result += mat4(0.12871371, 0.0064904913, 0.14985761, -0.10923005, 0.17413563, 0.1599109, -0.08457703, 0.108153716, -0.08871187, -0.06661137, 0.2754416, -0.009667768, 0.39819396, 0.12392097, 0.14145902, 0.0019376524) * go_0(0.0, -1.0); result += mat4(0.13893189, 0.12715353, 0.015191678, -0.21003054, -0.030412354, -0.01676613, -0.19799289, -0.006130075, 0.37676954, -0.14475077, -0.2065198, -0.30432892, -0.14944535, -0.09121536, -0.107600585, -0.24462196) * go_0(0.0, 0.0); result += mat4(-0.11653076, -0.0068671284, -0.02249137, -0.17877012, -0.15063138, -0.13514869, 0.107643366, -0.03196477, -0.086422764, 0.3079287, 0.17584166, -0.032449376, -0.06917114, -0.2682637, -0.18978168, -0.037039287) * go_0(0.0, 1.0); result += mat4(0.12014731, -0.030360512, -0.12954475, -0.110275604, -0.077214256, 0.019689744, 0.22149551, -0.002266716, 0.09697784, -0.124532826, -0.16776511, -0.034212478, -0.36935154, 0.016926935, 0.1363609, 0.20415346) * go_0(1.0, -1.0); result += mat4(-0.11199535, -0.001692563, -0.09058429, -0.08437503, 0.092625685, 0.06046257, 0.25509837, -0.011657033, -0.17949764, -0.10718947, -0.1180669, -0.24681842, -0.1747311, 0.0014518246, -0.042863015, 0.06103357) * go_0(1.0, 0.0); result += mat4(0.14979295, -0.037154514, 0.01957725, 0.012282435, 0.09168596, -0.05552286, 0.111671515, 0.0078630615, -0.10319766, -0.06416261, -0.23097566, -0.13931875, 0.2110811, 0.013095802, -0.2306504, -0.025639111) * go_0(1.0, 1.0); result += mat4(-0.10091975, -0.10095426, -0.023449723, -0.022170888, 0.054953706, -0.13049407, 0.08289061, 0.023241632, 0.08735388, -0.0058387457, 0.17897247, 0.011434436, 0.008181139, -0.0034718404, -0.015372735, -0.07657766) * go_1(-1.0, -1.0); result += mat4(-0.023442164, 0.07535702, 0.024391165, -0.050532013, 0.044168636, 0.0062343236, -0.019756999, -0.009695123, 0.10102337, 0.0052776975, -0.14944167, -0.060957722, 0.24367364, -0.08069369, 0.12170072, -0.047048368) * go_1(-1.0, 0.0); result += mat4(-0.18376935, -0.08407229, -0.12943378, 0.0738419, -0.12404976, -0.13367929, 0.11265896, -0.021353, 0.003783386, 0.50088304, 0.14058582, 0.041053623, 0.038247623, -0.014179976, 0.007905778, -0.042492237) * go_1(-1.0, 1.0); result += mat4(-0.046272535, 0.052449115, 0.17190954, -0.004745371, -0.045572635, -0.09292636, 0.36309823, 0.16673928, -0.099154025, -0.109614775, 0.17803112, 0.19907133, -0.14306267, 0.06898593, 0.11493454, 0.06795014) * go_1(0.0, -1.0); result += mat4(0.26181114, -0.044014625, -0.21605036, -0.08646438, 0.21038742, -0.084986, 0.0504626, 0.17514943, -0.25218952, -0.18691514, 0.057650108, 0.08653614, -0.101205684, 0.03176334, 0.18569492, 0.17973189) * go_1(0.0, 0.0); result += mat4(-0.0339215, 0.20112811, -0.12986277, 0.028961731, -0.056813832, 0.04451147, -0.07827432, -0.0860976, 0.096853435, 0.3483546, -0.35758162, -0.11749375, -0.035918653, 0.06140711, -0.08520154, 0.02418808) * go_1(0.0, 1.0); result += mat4(-0.09643022, -0.10491069, 0.0068604187, 0.023679713, 0.096521445, -0.29323488, 0.33353668, 0.112864286, -0.1172182, -0.07233183, 0.06607239, 0.08589609, 0.055790007, 0.14396138, -0.14191268, 0.00034840964) * go_1(1.0, -1.0); result += mat4(0.15357164, -0.038462736, 0.08143956, 0.1744909, 0.40503287, -0.114508316, 0.003937322, 0.2536635, -0.042445306, -0.15622465, 0.09155284, 0.010992155, -0.20646071, 0.022801135, 0.08894491, 0.069300614) * go_1(1.0, 0.0); result += mat4(-0.12663515, 0.023849454, -0.053604446, 0.12082873, -0.247968, -0.020969635, -0.03831894, -0.014617553, 0.22630337, 0.037801865, 0.052950703, 0.04285706, -0.14487264, 0.20786528, -0.08719664, 0.1752347) * go_1(1.0, 1.0); result += mat4(-0.073527604, -0.050752833, 0.051830504, 0.32868716, 0.17474994, 0.016937364, -0.08792601, -0.024481766, -0.022229593, 0.030706186, 0.09213566, -0.076506205, 0.073404044, 0.10368055, -0.175889, -0.08453031) * go_2(-1.0, -1.0); result += mat4(-0.06838216, 0.007698341, 0.063972116, -0.015604406, 0.16135305, 0.18044342, 0.024137018, -0.23326185, 0.13235588, -0.009096587, -0.058368143, -0.077040404, 0.0011419816, -0.09246194, 0.061036937, 0.049564146) * go_2(-1.0, 0.0); result += mat4(0.023225296, -0.00060856267, -0.07775185, 0.016958566, -0.2641349, -0.08263046, -0.15350416, -0.30203494, 0.113956556, -0.010813236, -0.017738314, -0.13689043, -0.10318342, 0.025793184, -0.010336172, 0.09733422) * go_2(-1.0, 1.0); result += mat4(-0.04462596, 0.052866418, -0.34754288, 0.05540498, -0.24492586, -0.32016864, 0.18145293, 0.24873725, 0.32388234, -0.034801524, -0.1347588, -0.07565546, 0.015183539, 0.05059595, 0.08090056, 0.05930932) * go_2(0.0, -1.0); result += mat4(0.045346696, -0.052527856, 0.052270077, 0.13417454, 0.05200045, 0.028119288, 0.005115497, 0.22952151, -0.2158375, 0.12241308, 0.3507457, 0.08616576, 0.07592416, 0.28470486, 0.3432788, 0.24857087) * go_2(0.0, 0.0); result += mat4(0.21311626, 0.052607164, 0.1248861, 0.20193806, 0.045226507, 0.14512901, -0.15103437, -0.17926466, 0.11657411, -0.32711068, -0.16332194, -0.07793982, -0.21802668, 0.5183869, -0.13567342, 0.07823041) * go_2(0.0, 1.0); result += mat4(0.00796368, 0.048073012, -0.14537893, -0.021708772, 0.036246423, 0.1062395, 0.12605369, 0.007073524, -0.1572743, 0.07439501, 0.089162275, -0.0039608316, 0.332032, -0.05461242, -0.17615359, -0.10240517) * go_2(1.0, -1.0); result += mat4(0.20636982, -0.0024615112, -0.10625786, 0.024270926, 0.061810836, -0.13585201, -0.16581286, 0.23549418, 0.01928842, 0.07404979, -0.054449487, 0.04096373, 0.046939734, 0.003980803, 0.02111498, 0.064925276) * go_2(1.0, 0.0); result += mat4(0.10485388, 0.06850885, -0.11292169, 0.16991565, -0.15282536, 0.124175504, -0.050431166, -0.06689582, -0.00059811946, 0.033696912, 0.11055047, 0.033060126, -0.17472714, 0.0048819613, -0.04478706, -0.1344572) * go_2(1.0, 1.0); result += mat4(-0.20473132, 0.056477875, 0.059559986, 0.115130566, -0.058425788, -0.035971727, 0.08334707, -0.096510135, -0.23206294, 0.10635798, -0.21575621, -0.07063254, 0.03877511, -0.107549034, 0.22248401, 0.21702304) * go_3(-1.0, -1.0); result += mat4(-0.02557767, 0.09886609, -0.100499466, 0.16687396, -0.084830604, 0.03150401, -0.049512494, 0.05595696, -0.13193256, -0.08585273, 0.14247662, 0.12290477, -0.07168309, 0.14531752, -0.048359327, 0.27716598) * go_3(-1.0, 0.0); result += mat4(0.13297586, 0.20674329, 0.14469388, 0.08981846, -0.004231366, -0.02819193, 0.15470329, 0.17299837, 0.113062344, -0.22716297, -0.21754944, -0.00083956274, -0.14160508, 0.1808253, 0.11268379, 0.27335623) * go_3(-1.0, 1.0); result += mat4(0.07497518, -0.06799594, -0.018158078, -0.00038999433, -0.15169668, -0.06928238, -0.33672288, -0.105485775, 0.33106267, 0.06698315, 0.019718744, -0.06810211, -0.35186404, -0.29145968, -0.056863394, 0.21498048) * go_3(0.0, -1.0); result += mat4(-0.013215512, -0.24763754, 0.20965266, 0.1068435, -0.13234195, 0.053566497, 0.05061848, -0.28645232, 0.15518288, 0.23247199, 0.017553907, -0.25181335, -0.048030723, -0.06663929, -0.111026704, -0.12663394) * go_3(0.0, 0.0); result += mat4(-0.010501938, -0.17995767, 0.06010859, 0.050185587, 0.108627126, -0.101203434, 0.07558728, 0.060466755, -0.106942676, -0.35854608, 0.16015992, 0.16823332, -0.06543775, -0.37310675, 0.014043972, -0.18328045) * go_3(0.0, 1.0); result += mat4(0.09712849, 0.013983463, 0.07291423, 0.031715546, 0.030862397, 0.045510456, -0.22066842, 0.063464865, 0.11721659, -0.10596602, -0.20611264, 0.052158818, -0.3961766, -0.03781582, 0.17633812, 0.1316111) * go_3(1.0, -1.0); result += mat4(-0.25029674, 0.07153423, -0.35125682, -0.18255402, -0.19569087, 0.00432772, -0.0969035, -0.24648514, -0.0040922165, 0.037500706, -0.038137026, 0.056214277, -0.048258524, 0.03567822, -0.05033007, -0.24696785) * go_3(1.0, 0.0); result += mat4(-0.03465209, -0.012495964, 0.22782089, 0.012034795, 0.2916752, 0.08264436, 0.15387125, -0.1473455, -0.15614432, 0.05536727, -0.027079755, 0.010725311, -0.03325222, -0.089212805, -0.10559839, -0.19647683) * go_3(1.0, 1.0); result += vec4(0.0001705175, -0.031081453, 0.010100773, -0.027214011); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf1 //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.026301445, -0.021575214, 0.22165509, 0.059994068, 0.03341161, 0.1831188, 0.20342293, 0.110160105, 0.03908121, 0.020673111, 0.07239561, 0.038754333, 0.15266368, 0.16526422, 0.062376205, -0.09759537) * go_0(-1.0, -1.0); result += mat4(0.19817191, 0.10267733, 0.17744653, 0.23283184, 0.18810122, 0.2708428, -0.12651879, 0.020756349, 0.039632563, -0.22201295, 0.04873703, 0.09159713, 0.13838065, 0.21169297, 0.30816007, 0.044463675) * go_0(-1.0, 0.0); result += mat4(-0.27859214, 0.07277634, 0.0021458792, 0.0089682285, -0.069680706, 0.090415835, -0.057762265, 0.18703683, -0.03514389, -0.102816254, -0.036509827, 0.038066104, -0.0168311, 0.094478935, 0.04079697, -0.049064912) * go_0(-1.0, 1.0); result += mat4(-0.20913245, -0.110538535, -0.08584027, -0.1222067, 0.05414807, -0.045247085, 0.07351766, -0.002078549, -0.1270987, -0.10164512, -0.1857815, 0.08845066, -0.03743333, -0.098948084, 0.21244387, 0.10441866) * go_0(0.0, -1.0); result += mat4(0.015990427, 0.36396438, -0.24094687, 0.30236533, -0.13271736, 0.06057376, -0.19678196, -0.28577125, -0.25427434, -0.08400598, 0.07284403, -0.18552442, -0.16425897, 0.097259276, -0.32386774, -0.2190484) * go_0(0.0, 0.0); result += mat4(-0.004581924, -0.13954072, -0.122360416, 0.14132866, -0.08529257, -0.013296556, 0.0848472, 0.09336581, 0.10332182, -0.016313016, 0.07103558, 0.032564916, -0.13478759, -0.20207484, 0.12986964, 0.1219679) * go_0(0.0, 1.0); result += mat4(0.09817874, -0.10573357, 0.100535244, 0.19608764, -0.13303067, 0.024192972, -0.030689823, 0.02574889, 0.051233094, 0.03489235, -0.18465245, -0.06943822, -0.031604882, 0.1519888, 0.09348508, 0.09187296) * go_0(1.0, -1.0); result += mat4(-0.21365458, -0.23696984, 0.13097638, -0.09435498, 0.16467983, -0.066370346, 0.1269104, -0.095128186, 0.09954892, 0.12489504, -0.43418056, 0.106512725, -0.17860703, -0.07114084, -0.07630834, -0.26642478) * go_0(1.0, 0.0); result += mat4(-0.009044342, 0.02711196, -0.14873673, 0.015405045, 0.0071443473, -0.025285944, 0.07409282, 0.06338527, 0.0149676185, 0.011741382, -0.2133069, -0.028912885, 0.19420496, 0.039629057, 0.057636812, 0.15214856) * go_0(1.0, 1.0); result += mat4(0.07629928, 0.25540486, -0.050925937, -0.18136702, 0.02261603, 0.22343902, 0.003270321, 0.10735731, -0.12541203, -0.10208828, 0.012832783, 0.2591262, 0.08122926, -0.009837677, 0.10308358, 0.19236866) * go_1(-1.0, -1.0); result += mat4(0.0896358, 0.27571487, 0.04406029, -0.047453407, -0.08587119, 0.16366854, 0.20622262, 0.08347545, -0.3501584, -0.28434548, -0.07592983, 0.09098784, 0.07605388, 0.09677056, 0.0015295541, 0.05102585) * go_1(-1.0, 0.0); result += mat4(0.18255898, 0.18618028, 0.0017002645, -0.013004655, -0.06436534, 0.13967068, 0.063077755, -0.10632303, -0.20803222, -0.028537111, -0.03144366, -0.08555215, 0.05154303, 0.02431626, 0.15246728, -0.013708507) * go_1(-1.0, 1.0); result += mat4(-0.020998938, -0.05026291, 0.03700117, 0.00830308, -0.1949294, 0.0026698054, -0.034649856, 0.19784226, -0.083901435, -0.069783084, -0.1504053, 0.16595264, -0.07480141, 0.16067508, 0.06010996, -0.021359695) * go_1(0.0, -1.0); result += mat4(-0.040828142, -0.20158486, 0.034770954, -0.1894161, 0.11665004, 0.29729164, -0.10584386, 0.13165873, -0.18863006, -0.26719162, -0.047613148, -0.12728356, -0.2033613, 0.10550052, 0.20095508, -0.11275811) * go_1(0.0, 0.0); result += mat4(-0.0785033, -0.1896073, -0.051492307, -0.1694358, 0.1368308, 0.049355216, -0.05707422, 0.079159185, 0.024578957, -0.0923136, 0.089215435, 0.28670043, 0.027932687, 0.06510816, 0.10810999, 0.05990052) * go_1(0.0, 1.0); result += mat4(0.08135192, 0.0001326522, -0.16098668, -0.18663193, -0.10280192, 0.078255914, 0.047648013, 0.08326376, 0.055962667, 0.06302574, -0.080121025, -0.031820554, -0.019117938, 0.12515336, 0.09794088, -0.03276838) * go_1(1.0, -1.0); result += mat4(0.280923, 0.24079335, 0.007883573, 0.06270414, 0.3055441, 0.19291803, -0.16041607, 0.14836526, 0.0013885222, 0.04538063, 0.10742898, -0.064491205, 0.048174977, 4.237692e-05, -0.15194727, 0.024381457) * go_1(1.0, 0.0); result += mat4(-0.0009164131, -0.031949926, 0.0076425644, -0.036870714, -0.0031292974, 0.017726978, -0.20172147, -0.0770472, 0.26379177, 0.108997814, 0.08069395, 0.2126177, 0.012075376, -0.029457828, 0.062730506, -0.15754452) * go_1(1.0, 1.0); result += mat4(0.09167904, -0.2657421, -0.03443356, 0.03315832, -0.015365421, -0.1029612, -0.108251, 0.04261033, -0.097120754, -0.05616668, -0.09275983, 0.024902184, 0.050058514, -0.013761632, 0.07555132, -0.0046676896) * go_2(-1.0, -1.0); result += mat4(-0.10743835, -0.0007361781, -0.042085417, -0.08237517, -0.10094376, -0.24007876, 0.13924706, -0.07526801, 0.01158322, 0.15491122, 0.0069442675, -0.004242352, 0.11429785, 0.02994726, -0.11829945, -0.04108612) * go_2(-1.0, 0.0); result += mat4(0.073622055, -0.064717196, -0.0025231615, 0.13256475, 0.20159899, 0.047977835, -0.10289233, -0.18419135, -0.00888952, 0.059428576, -0.053062655, -0.02730631, 0.14545685, -0.08686949, 0.17454128, 0.035443828) * go_2(-1.0, 1.0); result += mat4(-0.010146019, 0.06712568, 0.12614638, 0.023590917, 0.025756737, 0.06603747, -0.17108095, -0.06179699, 0.027241204, -0.13196802, 0.043475866, -0.0397495, 0.05306092, 0.035672903, 0.047219284, -0.16680142) * go_2(0.0, -1.0); result += mat4(0.079427816, -0.06716479, 0.19028603, -0.19694683, -0.061598092, -0.07471188, 0.21170339, 0.30140215, -0.0023369973, 0.04688297, -0.14154115, 0.19283508, 0.1339858, -0.09116279, 0.15305163, 0.029108394) * go_2(0.0, 0.0); result += mat4(-0.14902157, -0.03339153, -0.08532003, -0.10736339, 0.08702709, 0.07607574, -0.09955836, -0.016585784, -0.030078214, -0.060374748, -0.2854279, 0.02441719, 0.034877967, 0.2099041, 0.11125731, -0.059071556) * go_2(0.0, 1.0); result += mat4(-0.08436325, 0.06893047, -0.045362443, -0.02237741, -0.07583875, -0.034830183, -0.024008518, -0.2882329, -0.011109783, 0.101859994, 0.091137715, 0.0020565533, -0.044729806, -0.18168025, 0.069466636, 0.04994174) * go_2(1.0, -1.0); result += mat4(0.11915174, 0.089596465, -0.18965814, 0.015218237, 0.13500094, 0.19921367, -0.008298205, 0.29650384, -0.049439427, -0.27590424, 0.36169067, -0.030582754, 0.02151196, 0.019915426, 0.04543398, 0.16126189) * go_2(1.0, 0.0); result += mat4(0.1620274, -0.08264547, 0.082442135, -0.0034478644, 0.09888509, -0.0034957859, -0.107241705, -0.17729597, -0.05138647, 0.02052103, -0.019507123, 0.037574988, -0.1694345, 0.17871588, -0.22510391, 0.019049853) * go_2(1.0, 1.0); result += mat4(-0.10962245, -0.1329873, -0.060855392, 0.025941676, -0.19536193, -0.120365486, -0.04313703, -0.052912965, 0.20854498, 0.08341353, 0.008687068, -0.20432276, 0.15677948, -0.19000018, 0.01821201, -0.041512605) * go_3(-1.0, -1.0); result += mat4(0.012287526, -0.14180368, -0.098788455, 0.025949089, 0.09442778, 0.2247651, -0.12453263, 0.10435483, 0.274603, 0.06133054, 0.10506106, 0.14727746, -0.048299775, -0.082819685, 0.07319359, -0.047460355) * go_3(-1.0, 0.0); result += mat4(-0.070726536, -0.034744017, 0.07521428, 0.070649154, -0.05958955, -0.100232825, -0.010651838, 0.045392875, 0.2930271, -0.04952355, 0.3112155, 0.117203265, 0.025166962, 0.11176862, 0.06716659, 0.07175864) * go_3(-1.0, 1.0); result += mat4(-0.011560962, -0.14032063, -0.17424704, 0.07652749, -0.04220116, 0.052874275, -0.00225693, -0.031843517, -0.07520102, -0.13775803, 0.2449317, 0.069658786, 0.052280303, -0.105218224, 0.03574522, -0.020500354) * go_3(0.0, -1.0); result += mat4(0.08793712, 0.26712346, 0.08315631, 0.23813692, -0.04439029, 0.031587064, 0.09561177, -0.13380238, -0.24982157, 0.31701845, -0.3875432, 0.10487225, 0.09201869, -0.037252493, -0.006935219, -0.14650282) * go_3(0.0, 0.0); result += mat4(0.077635325, 0.13732299, -0.071563005, 0.096517466, -0.15051986, -0.111744404, 0.03996857, -0.052670125, -0.1819665, 0.054554947, -0.13774712, -0.20061246, -0.0023742192, 0.15647805, -0.024121126, 0.075497724) * go_3(0.0, 1.0); result += mat4(0.0073632775, -0.06535298, 0.039895996, 0.20666869, 0.13625242, 0.04823007, -0.07135618, 0.04787906, 0.01383074, 0.15382123, -0.15519714, 0.056721795, 0.061946746, -0.0586851, 0.028934354, -0.02264129) * go_3(1.0, -1.0); result += mat4(-0.19791882, -0.111910924, -0.010451344, -0.30566537, -0.1416239, -0.14523096, 0.116883226, -0.18241516, 0.2680614, -0.18487626, 0.17472346, 0.08346682, -0.14510359, -0.029229192, -0.005879142, 0.050247498) * go_3(1.0, 0.0); result += mat4(0.030153519, -0.092469186, -0.022912916, 0.10200855, -0.04237032, -0.05917764, 0.10479645, -0.05619482, -0.18949397, -0.019547248, 0.013868889, -0.1524476, 0.14048979, -0.032521486, 0.1322921, 0.070972025) * go_3(1.0, 1.0); result += vec4(0.012053958, -4.6962363e-05, 0.0020099226, -0.033494607); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.06738501, 0.034009207, -0.21538448, 0.14296548, 0.12896985, -0.23526315, -0.08848608, 0.019602662, 0.14937137, 0.11353096, 0.11884168, -0.016765572, 0.030985225, 0.046430565, 0.06614828, -0.19202724) * go_0(-1.0, -1.0); result += mat4(-0.10326068, 0.11014975, 0.17069744, -0.21474148, 0.16761585, 0.13434832, -0.101021074, 0.006307025, 0.07478008, -0.1060066, 0.035315692, 0.033488914, -0.24906659, 0.06269967, 0.11120735, -0.040928528) * go_0(-1.0, 0.0); result += mat4(0.09334615, 0.057705753, 0.12213245, -0.06402275, 0.30694544, 0.034585163, 0.20345578, 0.07489286, 0.07483618, -0.14240396, 0.034846418, -0.03811241, 0.010882573, 0.13204294, 0.017563924, -0.047203008) * go_0(-1.0, 1.0); result += mat4(-0.21673942, -0.024010994, -0.10238504, -0.041160326, 0.06838163, -0.20950818, 0.06526309, -0.079094924, 0.02208821, -0.28130978, 0.086275116, -0.089067616, 0.12133826, -0.062600106, -0.020521903, -0.07654401) * go_0(0.0, -1.0); result += mat4(-0.03055029, -0.15683146, -0.20331301, -0.06252028, 0.13350682, 0.20338707, 0.038425338, 0.1581342, -0.27322498, -0.14999662, -0.16681097, 0.0971585, -0.20014858, -0.081635274, -0.0781877, -0.20625232) * go_0(0.0, 0.0); result += mat4(0.38375977, -0.019825654, 0.1886721, 0.22616312, 0.3402173, 0.1825304, -0.05531195, 0.30973226, -0.2676023, 0.14413352, 0.021706983, 0.01732799, 0.23466855, -0.13805965, 0.22570935, 0.018103868) * go_0(0.0, 1.0); result += mat4(-0.15169825, 0.0270689, -0.2503316, 0.17289825, -0.16437647, 0.039233048, -0.35572487, -0.048393793, 0.19270042, 0.24260359, 0.12041881, -0.0009793913, 0.11656858, 0.11007414, -0.0757491, 0.047933612) * go_0(1.0, -1.0); result += mat4(-0.18657999, -0.11252566, -0.05237504, -0.07368097, 0.13882741, -0.13710637, -0.006996468, -0.062354874, 0.23452504, 0.15333645, -0.0022776406, -0.17910439, 0.03629509, -0.16264829, -0.010011833, -0.15313338) * go_0(1.0, 0.0); result += mat4(-0.060544558, -0.04913478, -0.061717357, 0.02323648, 0.28739056, -0.07434013, 0.19110644, 0.100050166, 0.0073363045, 0.08185653, -0.024797903, -0.14424153, -0.20838726, 0.16154376, -0.048517212, -0.025453888) * go_0(1.0, 1.0); result += mat4(0.14975396, -0.13142908, 0.36210674, -0.054021083, -0.10632155, 0.045697935, -0.18946633, 0.02228141, -0.08919603, 0.09800842, -0.17634438, 0.09512711, -0.03425503, -0.12298555, -0.05354435, -0.17112055) * go_1(-1.0, -1.0); result += mat4(0.09958265, -0.057276618, -0.16262266, -0.06415915, 0.14579074, -0.36784375, 0.08034197, -0.04537706, 0.005460582, 0.22313322, 0.07382161, 0.014990379, 0.044636846, -0.2811128, -0.22621547, -0.06044004) * go_1(-1.0, 0.0); result += mat4(0.10569276, -0.03738662, 0.16100396, 0.058593616, -0.048862137, -0.08796426, 0.20101094, -0.11039573, 0.17196764, -0.04601554, 0.008571281, -0.073729075, 0.051433694, -0.051276565, 0.087334655, -0.0360379) * go_1(-1.0, 1.0); result += mat4(0.011119538, -0.28781965, 0.28637868, -0.1742508, -0.07121849, 0.10379717, 0.012615981, -0.029563965, -0.18678424, 0.05291095, 0.039143506, -0.028248642, -0.014103922, 0.029155696, 0.10433492, 0.16305852) * go_1(0.0, -1.0); result += mat4(-0.2231037, -0.13697462, -0.29124337, 0.08519773, 0.15893684, -0.17763218, 0.06950923, 0.34361118, -0.024844287, 0.044008408, -0.033844844, -0.086971916, -0.07884748, 0.2543499, 0.056884114, 0.10068364) * go_1(0.0, 0.0); result += mat4(-0.07710048, -0.23218372, 0.04346047, 0.21769643, 0.06473219, -0.18066105, -0.2511205, 0.15309611, 0.04535977, 0.16450433, 0.10846344, 0.0016952346, -0.010874939, 0.28966382, -0.121990964, 0.12956186) * go_1(0.0, 1.0); result += mat4(-0.007910202, 0.17766511, 0.14364475, 0.1016258, 0.0051045395, 0.18691733, 0.005813767, -0.0070582186, 0.019418601, -0.1604435, 0.016088275, -0.18265302, -0.15719391, -0.17369832, -0.036745597, -0.19647408) * go_1(1.0, -1.0); result += mat4(0.08938396, -0.0073808245, 0.11225727, -0.012303106, 0.096785046, 0.030483445, 0.027719889, -0.052584838, -0.14887555, -0.03422243, 0.12646855, -0.1722482, 0.010239037, 0.06406088, -0.20053658, 0.01964698) * go_1(1.0, 0.0); result += mat4(-0.120734036, -0.12450362, -0.06582111, 0.1639675, -0.19787048, -0.08049789, -0.014257596, 0.058436662, -0.0009387449, -0.08698089, -0.017400503, 0.06295286, 0.09890349, -0.057190523, -0.103520766, -0.04207548) * go_1(1.0, 1.0); result += mat4(-0.0118413875, -0.031288836, 0.09749554, -0.012266401, -0.07998591, 0.22615653, -0.06207416, 0.03257896, -0.076378696, -0.079426095, -0.13968349, -0.15423697, -0.1091681, -0.02893125, -0.032659534, -0.063735925) * go_2(-1.0, -1.0); result += mat4(0.119372696, 0.013176554, -0.029381052, 0.21919228, 0.045041792, 0.24844484, 0.26363325, 0.08480674, 0.087083444, 0.11984778, -0.088715754, 0.06421046, 0.05225977, -0.05140334, -0.055052705, -0.049854077) * go_2(-1.0, 0.0); result += mat4(0.0035781674, 0.0861361, -0.07675145, -0.056479637, 0.16973391, -0.12113791, 0.10729832, -0.03773517, 0.058618728, 0.12148276, 0.17260705, -0.06968724, 0.076358154, -0.15307103, 0.17700425, -0.13467014) * go_2(-1.0, 1.0); result += mat4(-0.02752418, -0.06366472, -0.025610954, 0.0013539721, -0.06465272, 0.0806373, -0.07336035, 0.10114861, 0.0041146413, 0.15878421, -0.044668555, -0.12150811, -0.1071482, -0.05086587, 0.18589285, 0.05065092) * go_2(0.0, -1.0); result += mat4(0.07200056, 0.021739854, 0.29476613, -0.08475931, 0.15018553, -0.07886365, 0.36336347, -0.020576432, 0.25866082, -0.059272554, 0.054249667, -0.17822553, 0.1755872, 0.3244387, -0.39173844, 0.33894604) * go_2(0.0, 0.0); result += mat4(-0.11570926, 0.1342677, -0.19511898, 0.0075454637, -0.01890476, -0.14239742, 0.18921931, 0.033990458, 0.31306365, -0.006998358, 0.029190077, -0.005679954, -0.15341778, 0.07766778, -0.25691047, -0.0964161) * go_2(0.0, 1.0); result += mat4(0.019746238, 0.0021332854, -0.00879096, -0.1338671, -0.0001600663, -0.29465106, 0.0867611, -0.114963025, 0.07874301, -0.012734178, -0.11124061, -0.010926616, -0.04941506, -0.07516841, 0.116663, -0.29018974) * go_2(1.0, -1.0); result += mat4(-0.01651721, 0.05955898, 0.023618208, 0.098695934, 0.018553663, -0.054378513, 0.1436929, 0.1693743, -0.27483663, 0.029127488, 0.09619316, -0.06109113, -0.08619361, 0.09315214, -0.02478657, 0.18544984) * go_2(1.0, 0.0); result += mat4(0.09570196, -0.016528936, -0.1559397, 0.14312246, 0.04029428, 0.08773151, -0.043646842, 0.17894371, -0.082413055, 0.0027082344, -0.100171275, 0.01547501, 0.18122818, -0.11933676, 0.26404107, -0.3169703) * go_2(1.0, 1.0); result += mat4(-0.12073344, 0.08683522, -0.09249099, 0.058786053, -0.14480567, -0.121013954, 0.033335857, 0.009353379, -0.055087596, -0.13002734, 0.08890566, 0.05508963, -0.0075715426, -0.15936922, -0.03968994, -0.1690259) * go_3(-1.0, -1.0); result += mat4(0.2011206, 0.23898427, 0.23656492, 0.1287573, 0.14850396, 0.40532517, -0.107408255, 0.40119782, 0.099813245, -0.03830304, 0.101520434, -0.026478073, -0.048469637, 0.106440455, 0.056632314, -0.17825997) * go_3(-1.0, 0.0); result += mat4(-0.076735444, 0.05965795, -0.0052469415, -0.21785147, 0.11887833, 0.067560315, 0.051149055, 0.23626682, -0.1297049, -0.035512198, 0.20352256, -0.025064934, 0.04958706, 0.0454198, 0.0113334535, 0.0417486) * go_3(-1.0, 1.0); result += mat4(-0.09055751, 0.033915352, -0.21836667, 0.22006813, -0.099022895, 0.11720966, -0.15686816, -0.13586599, -0.094427735, -0.08831514, -0.06182928, 0.09213704, -0.03642064, 0.18129414, -0.012926811, 0.12179882) * go_3(0.0, -1.0); result += mat4(0.19389409, 0.09512252, 0.14768016, -0.16623649, -0.031052284, -0.026814984, 0.106168024, -0.2026781, -0.04581419, -0.0016849053, -0.04101923, 0.038959503, -0.011938445, 0.20096186, -0.26666564, 0.4824324) * go_3(0.0, 0.0); result += mat4(0.17727576, 0.07309147, 0.12131863, -0.163096, 0.17225246, 0.26256254, 0.27685758, 0.09094053, 0.029605515, -0.20217367, 0.047564875, 0.043115832, 0.15089568, -0.09670934, 0.24131384, 0.03337442) * go_3(0.0, 1.0); result += mat4(-0.34192136, 0.12063195, -0.31159517, 0.04170889, -0.30147067, -0.21330686, -0.1514457, -0.121126845, 0.04409098, 9.2206596e-05, 0.027680017, 0.03230512, -0.27993527, -0.093485355, 0.07568645, -0.23585452) * go_3(1.0, -1.0); result += mat4(0.0537712, -0.20847629, 0.1740093, -0.013894753, -0.32719997, -0.059484575, -0.006098233, -0.10336451, -0.14706188, -0.07424865, -0.07045905, 0.17093194, -0.22147557, 0.09086218, -0.11033544, -0.05306482) * go_3(1.0, 0.0); result += mat4(0.00489003, -0.11509064, -0.021005848, 0.16637677, -0.089347586, 0.17545725, -0.17313693, 0.13742085, -0.14577347, 0.07951095, -0.092139855, 0.017118992, -0.053472433, 0.079414465, 0.0330263, -0.11189824) * go_3(1.0, 1.0); result += vec4(-0.034743138, 0.012946433, -0.082333155, 0.07721756); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf1 //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.25835788, 0.050451655, -0.1845038, -0.07232528, 0.1323318, 0.26276684, 0.10842882, -0.083056524, 0.17426784, -0.3594826, 0.2728965, 0.08388844, -0.004007842, 0.020535901, -0.051425606, 0.07750436) * go_0(-1.0, -1.0); result += mat4(-0.11410436, 0.014572361, -0.27057216, -0.023974562, 0.05234827, 0.15328228, -0.17502303, -0.3199359, 0.12188045, -0.095813684, 0.024145132, 0.0856916, -0.027453909, -0.043129764, 0.16971985, 0.021623038) * go_0(-1.0, 0.0); result += mat4(0.06611095, 0.038625732, -0.13717118, -0.04497733, 0.15213469, 0.04770935, 0.0729271, -0.062052976, 0.004571303, 0.035141192, -0.059409596, 0.044652313, 0.17520894, 0.09665589, -0.1479193, 0.06528058) * go_0(-1.0, 1.0); result += mat4(-0.1845968, 0.091479465, -0.09394898, -0.13545018, -0.029501775, -0.21426639, 0.09255898, 0.1257644, 0.20256902, 0.06267267, 0.10378081, 0.13494423, 0.058310498, 0.03642236, -0.16268995, -0.048100803) * go_0(0.0, -1.0); result += mat4(0.2155119, -0.3683131, 0.049449228, -0.20559964, -0.11761922, -0.2518804, -0.020712897, 0.12895772, -0.07543782, 0.5805017, -0.11301444, -0.038493153, -0.06710986, -0.09321189, 0.108671665, -0.03259695) * go_0(0.0, 0.0); result += mat4(0.035307787, 0.108389005, -0.27493554, 0.27029404, 0.25523573, -0.28636125, -0.20766719, -0.008661457, -0.004480811, -0.046390545, -0.16221444, 0.008979624, -0.061375532, 0.035076566, -0.018924266, 0.01380219) * go_0(0.0, 1.0); result += mat4(-0.051922515, -0.12463486, -0.10383422, 0.02220095, -0.1573033, 0.13980615, 0.13248625, -0.16803266, -0.0692132, -0.21552645, 0.13744529, 0.23034313, 0.0052666534, 0.028977966, 0.07720251, -0.06477756) * go_0(1.0, -1.0); result += mat4(-0.14097473, 0.2770271, -0.172289, -0.03000696, -0.028684044, 0.040578447, -0.2290285, 0.082329154, -0.042402364, -0.20926563, 0.08233207, 0.11862443, -0.07038536, -0.02273004, 0.091550544, -0.065856494) * go_0(1.0, 0.0); result += mat4(0.14879914, -0.023923844, -0.23569296, 0.20306346, 0.17502785, 0.28776234, -0.2788995, 0.10012439, -0.05635638, -0.025840463, 0.09222198, 0.118032, 0.08057015, 0.1286071, 0.060189806, -0.052669708) * go_0(1.0, 1.0); result += mat4(0.07076086, -0.15111323, -0.07427972, 0.008372168, -0.17791592, -0.16254742, 0.013961132, -0.0944912, -0.23380096, 0.17377278, -0.09683394, 0.019931393, -0.12042098, 0.0016406325, 0.09393333, -0.06882231) * go_1(-1.0, -1.0); result += mat4(0.21465093, 0.04142968, 0.06840044, -0.37831602, -0.05549571, 0.044905066, -0.07873589, -0.026804, -0.34764197, 0.022487951, -0.077293746, 0.089457795, -0.110094436, 0.24233972, 0.06285107, -0.10851744) * go_1(-1.0, 0.0); result += mat4(0.093270175, 0.084138945, 0.03938272, 0.063565865, -0.010733802, 0.13554469, -0.06650261, 0.033002816, 0.011187271, -0.12821455, 0.20785914, -0.030438649, -0.124710515, -0.022294303, 0.09732408, 0.057609864) * go_1(-1.0, 1.0); result += mat4(-0.12833868, 0.021577539, -0.02700365, 0.11799592, -0.03655647, -0.04225167, 0.11049353, -0.16036157, 0.049277548, -0.033842396, 0.10020137, 0.095509745, 0.08060231, -0.09237418, -0.035598125, -0.035926737) * go_1(0.0, -1.0); result += mat4(-0.32829186, 0.3492363, 0.030671779, -0.12606762, 0.010437313, 0.2757115, -0.21517593, -0.15800527, -0.12592544, -0.20578934, 0.10444053, 0.12993255, -0.046079267, 0.03834173, -0.19277227, -0.22124454) * go_1(0.0, 0.0); result += mat4(-0.052546192, 0.026082167, 0.13831234, 0.10982424, 0.012946818, -0.12439852, 0.10134106, -0.10050398, -0.04472338, -0.14325236, -0.20579574, 0.0044005127, 0.22013672, -0.32955512, 0.12404084, -0.008160738) * go_1(0.0, 1.0); result += mat4(-0.10774314, -0.31650826, -0.06601711, 0.19635755, -0.12622592, -0.06396423, 0.13856032, 0.16540553, 0.021387719, 0.23377723, -0.053738154, -0.1000186, -0.08338395, -0.052813534, 0.008122962, 0.13732094) * go_1(1.0, -1.0); result += mat4(-0.18270823, 0.06966014, -0.17788303, -0.27303055, -0.077971615, 0.013978423, -0.02039098, 0.12715338, -0.11924171, 0.18900296, -0.085199654, 0.215198, 0.18587974, -0.009749325, 0.0173584, -0.12018259) * go_1(1.0, 0.0); result += mat4(0.052129295, -0.107416354, 0.12711766, 0.03708665, -0.14369462, -0.055359814, -0.16639823, -0.045143317, -0.06925672, -0.040696755, 0.01999809, -0.016040625, -0.02484878, 0.07417094, 0.050875198, 0.2145528) * go_1(1.0, 1.0); result += mat4(0.055696912, -0.16680926, -0.021987487, 0.024941636, -0.0927883, 0.022136632, 0.033782948, -0.10646058, -0.14944647, 0.25457275, 0.046682496, -0.022462368, -0.07886781, 0.08165927, 0.06848105, 0.0063734027) * go_2(-1.0, -1.0); result += mat4(0.037053242, 0.033215813, 0.18291366, 0.12340375, 0.08491059, -0.28442004, -0.0127422465, -0.039834313, -0.23321372, 0.26676926, -0.05636355, -0.15672484, -0.12891728, -0.15486577, -0.032004442, -0.092745155) * go_2(-1.0, 0.0); result += mat4(0.015779478, -0.18457565, 0.24996394, 0.036197674, 0.15694007, 0.15863103, -0.07332398, 0.0016235278, -0.15536517, -0.056062788, 0.14102836, 0.16915025, -0.08001087, 0.07073164, 0.13796777, 0.123867124) * go_2(-1.0, 1.0); result += mat4(0.045792986, -0.15135059, -0.1354885, -0.043678258, -0.35655212, 0.51232076, -0.12816145, -0.046569496, -0.014127674, -0.06282611, -0.098873, -0.06359104, -0.0919222, 0.11822437, 0.079254694, 0.00579688) * go_2(0.0, -1.0); result += mat4(-0.15683417, 0.61610246, -0.3024612, 0.12917964, -0.09303367, 0.23612969, -0.40842506, -0.12374661, -0.07572449, -0.2613284, -0.09970177, -0.015227848, 0.106239066, -0.21411185, 0.051998455, -0.1364518) * go_2(0.0, 0.0); result += mat4(0.23850034, -0.14394449, -0.0031468747, -0.2380617, -0.027200876, -0.041352056, -0.01864445, 0.033848196, -0.12064239, -0.110480845, 0.08450956, -0.22328654, 0.17664163, 0.22268307, 0.050886698, -0.17475672) * go_2(0.0, 1.0); result += mat4(-0.17808256, 0.010803805, 0.03315186, 0.033143792, -0.14205995, 0.25039625, -0.08784382, -0.13454252, 0.19576813, 0.10755282, 0.22821628, 0.019456752, -0.0422955, -0.016182603, -0.12066697, 0.0548465) * go_2(1.0, -1.0); result += mat4(0.11563777, -0.257929, 0.0010403778, 0.080267854, -0.0025255163, 0.2855168, -0.060352214, -0.07816255, -0.00090574916, 0.049510725, 0.03720483, 0.059250016, -0.08674136, 0.20522198, -0.28694284, 0.1299507) * go_2(1.0, 0.0); result += mat4(-0.14638457, 0.04063328, 0.03139636, -0.007934521, 0.07689684, -0.09467145, 0.10607347, 0.054510128, 0.003306194, 0.05347124, 0.062762424, -0.041480847, -0.07677865, -0.139573, 0.010972524, 0.21957156) * go_2(1.0, 1.0); result += mat4(-0.026845628, -0.043439507, 0.034738723, 0.07281683, 0.14474197, 0.031586993, -0.22767854, -0.0707655, 0.105201736, -0.28805482, 0.008668302, -0.16329518, 0.06157049, 0.3803886, 0.26345953, -0.011096537) * go_3(-1.0, -1.0); result += mat4(-0.23328833, 0.085731484, -0.07755016, 0.33559516, 0.07704345, 0.115106605, -0.24114038, -0.44630137, 0.2726737, -0.32170138, -0.009236524, -0.11666051, 0.0457048, 0.07876708, 0.13134004, -0.035318643) * go_3(-1.0, 0.0); result += mat4(-0.05140272, 0.011605703, 0.13899171, -0.05071015, 0.18413687, -0.31413674, -0.13043414, -0.15118152, -0.15326938, -0.10720126, -0.23738635, 0.13481396, 0.25115076, -0.009316611, -0.2584441, -0.14389823) * go_3(-1.0, 1.0); result += mat4(-0.039723795, -0.14869407, -0.1692942, 0.026501274, -0.10685166, -0.121267825, -0.08584318, -0.09580693, -0.10626739, -0.068417974, 0.11321909, -0.13664317, 0.061380867, -0.2587898, 0.14850819, 0.008178645) * go_3(0.0, -1.0); result += mat4(0.06912782, 0.24230564, -0.048150286, 0.2203717, -0.17417085, 0.105546735, -0.16648416, -0.0045053074, 0.09764028, 0.37122592, -0.1939995, -0.27899942, -0.088152565, -0.53869057, 0.21676709, -0.08056594) * go_3(0.0, 0.0); result += mat4(0.07651754, 0.03704878, -0.0197015, 0.1660726, 0.07002748, -0.11820414, -0.23360898, 0.1481592, 0.029847002, 0.054057185, 0.013176299, 0.06552942, -0.13865773, -0.20105527, -0.37550658, 0.005769631) * go_3(0.0, 1.0); result += mat4(-0.22697811, -0.17426412, 0.10148018, 0.008134666, 0.10771455, 0.16943407, -0.016319012, -0.40176705, -0.06854668, -0.049045276, 0.20919096, 0.13240765, -0.050125647, 0.14902508, 0.052697595, -0.13817468) * go_3(1.0, -1.0); result += mat4(0.04301619, 0.23184754, -0.023551717, 0.3768405, 0.028999053, 0.06709736, -0.05993663, -0.059861984, 0.15499207, -0.22217415, 0.111131504, -0.09082529, -0.19389243, 0.024621522, -0.15305442, 0.010799284) * go_3(1.0, 0.0); result += mat4(-0.035496738, 0.010802548, -0.028718363, 0.19263634, 0.16900502, -0.16661702, -0.027631328, 0.18309957, -0.015860107, -0.03309961, -0.091390446, 0.14000848, -0.0036591904, 0.47659522, -0.09373507, -0.29020965) * go_3(1.0, 1.0); result += vec4(0.08895955, -0.027667087, 0.20500831, 0.00037762933); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.018134737, -0.2296755, -0.07276725, -0.029795367, 0.05382051, 0.092847414, -0.024469728, -0.1674685, 0.0017946451, 0.30074653, 0.0034195695, -0.04892261, 0.18229689, -0.20116119, -0.12702174, -0.08259108) * go_0(-1.0, -1.0); result += mat4(-0.1357695, -0.08149211, 0.09314453, -0.21966846, 0.34740716, 0.043606415, 0.04225903, 0.034449834, 0.17248215, 0.39148283, -0.13868807, -0.010550686, 0.044238456, -0.09693464, -0.005044985, 0.24383289) * go_0(-1.0, 0.0); result += mat4(0.19959371, 0.098685324, 0.058746945, 0.010580748, 0.08051514, 0.031898864, 0.017556064, 0.13004355, -0.01727653, 0.11044019, 0.040673427, -0.20064595, -0.23321067, 0.06398686, -0.19126236, -0.2430858) * go_0(-1.0, 1.0); result += mat4(-0.12870286, -0.113455534, 0.23722827, 0.070718594, 0.19049989, -0.1927299, -0.06343845, 0.113127775, 0.082530305, -0.10972526, -0.090779535, 0.05731582, 0.11018802, -0.18049154, 0.09269507, -0.10304576) * go_0(0.0, -1.0); result += mat4(0.15513484, 0.06659583, 0.08125296, -0.012350324, -0.09492788, 0.5048303, 0.13206847, 0.39554298, 0.28953737, -0.20913891, -0.26781562, -0.17539899, 0.023778774, 0.29716817, 0.15768486, 0.37702608) * go_0(0.0, 0.0); result += mat4(0.0724462, 0.015571356, -0.032217246, 0.0050658924, -0.22708446, 0.03968809, 0.016753826, 0.0025668752, -0.055932112, 0.113931604, 0.19766758, -0.030027265, -0.17384295, 0.15013468, -0.0070017707, -0.09469028) * go_0(0.0, 1.0); result += mat4(-0.078361556, -0.0954201, -0.006358101, 0.040500037, 0.4190454, -0.17622913, -0.07234791, 0.05462559, 0.18641087, 0.058313597, -0.0180785, 0.13818781, -0.14640772, 0.0699486, 0.0073663946, -0.076789856) * go_0(1.0, -1.0); result += mat4(-0.21421191, 0.08736062, 0.09041226, 0.03608585, 0.02769972, 0.09641289, 0.11824623, 0.05653645, 0.16464607, 0.19839554, -0.13379547, 0.054417104, 0.067530684, 0.18971571, 0.13785432, -0.097639814) * go_0(1.0, 0.0); result += mat4(-0.32658005, -0.14606023, -0.069448665, 0.032998275, -0.28331423, 0.0011900732, -0.020304207, -0.13535896, 0.08298347, 0.045509677, -0.030503955, -0.037504148, 0.049955815, 0.0925771, 0.00058534974, -0.12398032) * go_0(1.0, 1.0); result += mat4(-0.2955836, 0.29059318, -0.018196672, -0.35866606, -0.01309431, 0.03540315, 0.010609202, 0.11956812, 0.10296229, 0.22536302, 0.015201129, -0.23797737, -0.16960852, -0.11414787, -0.034440614, 0.112644605) * go_1(-1.0, -1.0); result += mat4(-0.14952518, 0.07024436, -0.083184876, -0.0814617, -0.13303639, 0.016159372, -0.13521518, 0.2221334, -0.056617837, 0.12958299, 0.064461656, -0.20146395, -0.16023181, 0.2640758, 0.27528805, -0.1426518) * go_1(-1.0, 0.0); result += mat4(-0.04382363, 0.09856003, -0.08561442, -0.15699928, -0.121069774, 0.04685383, -0.009170197, -0.031489655, 0.18730178, 0.238442, 0.22497098, 0.032015145, -0.03709115, 0.1535079, 0.21674158, 0.10678019) * go_1(-1.0, 1.0); result += mat4(-0.12200952, 0.24224263, 0.034097504, -0.028179523, -0.011962496, -0.04489487, -0.05198827, 0.22194928, -0.045400873, -0.049828544, 0.111477956, -0.098361604, 0.12788995, -0.016093334, -0.19886433, -0.011161484) * go_1(0.0, -1.0); result += mat4(0.30563712, 0.013071727, -0.004799883, 0.12888052, -0.259498, -0.041566677, 0.07311124, 0.162324, 0.28371668, -0.004693743, -0.0019395344, 0.029358242, 0.08730285, 0.12184509, 0.05508437, 0.048439097) * go_1(0.0, 0.0); result += mat4(0.12760857, 0.115813166, -0.217695, -0.10629871, -0.227366, 0.09030426, -0.15313712, 0.020528946, -0.20743734, 0.088583544, 0.04594053, -0.22891994, 0.18949282, -0.042186577, -0.17330512, -0.010711361) * go_1(0.0, 1.0); result += mat4(0.029503195, 0.0063797613, -0.17004286, -0.096844055, 0.010218098, 0.04247233, 0.02362808, 0.14700809, -0.08082364, 0.11159672, -0.018505255, -0.15228583, 0.15693732, -0.025359154, 0.024829186, 0.1943192) * go_1(1.0, -1.0); result += mat4(-0.03912932, -0.21989027, 0.12203028, 0.18702275, -0.118537985, 0.21039696, 0.09102061, 0.012288879, 0.031666897, 0.1318455, -0.04901404, -0.07516063, -0.44782668, 0.04884501, 0.047070876, 0.008728358) * go_1(1.0, 0.0); result += mat4(-0.08669101, 0.3053463, -0.08963947, 0.0034188698, -0.070004664, 0.064788476, 0.093737036, 0.070050925, 0.12728429, -0.13179256, -0.014913502, 0.09308136, -0.027638942, 0.008638711, 0.08794172, -0.05531093) * go_1(1.0, 1.0); result += mat4(0.0728421, 0.07872358, 0.11454748, 0.08497922, 0.071820416, -0.11789207, -0.08184197, 0.1359588, -0.2143346, -0.05876081, 0.023172129, -0.08430511, -0.19276723, 0.14283359, 0.15604696, -0.055187486) * go_2(-1.0, -1.0); result += mat4(0.068641685, 0.2732106, -0.2809107, 0.12736696, -0.08642367, 0.023898933, -0.17859498, -0.18299665, -0.06684587, -0.12204666, 0.45898953, -0.24240111, 0.25182098, -0.04395751, 0.10637211, -0.22135144) * go_2(-1.0, 0.0); result += mat4(0.0852072, 0.051133018, 0.03333165, -0.0008938216, 0.10251267, 0.0550774, 0.041769378, -0.21259712, 0.286912, 0.123342015, 0.282759, -0.0730124, 0.14275575, -0.15580742, -0.15224406, 0.045376908) * go_2(-1.0, 1.0); result += mat4(0.03328225, 0.11563978, -0.07451964, 0.030546209, -0.04698351, -0.18544962, 0.037350416, 0.13969816, 0.0556746, -0.06359919, 0.06478219, -0.031694926, 0.13396506, 0.09443612, -0.01922686, -0.06290365) * go_2(0.0, -1.0); result += mat4(0.07495407, 0.063429266, -0.106221214, -0.085107304, 0.2497817, -0.46598253, -0.18833177, -0.2731128, -0.13024822, 0.56053543, 0.055704467, -0.12331414, -0.031199086, 0.05061188, 0.22097112, -0.6611177) * go_2(0.0, 0.0); result += mat4(0.08276988, -0.044184342, -0.03562185, -0.06159881, 0.27694225, -0.07192965, -0.08663714, 0.020221777, 0.14095962, -0.06229397, 0.051374253, -0.038158998, 0.10664802, -0.041305423, 0.051260717, -0.054698635) * go_2(0.0, 1.0); result += mat4(0.12800686, 0.03485072, 0.039914366, 0.034041498, -0.08305794, -0.046292894, 0.22765331, 0.10904922, 0.0013937047, -0.08750301, 0.009126207, -0.065589435, 0.2837707, 0.08884436, -0.07234862, -0.093502745) * go_2(1.0, -1.0); result += mat4(0.113439895, 0.06081726, 0.1122302, -0.022936966, 0.10329637, -0.31816107, -0.051597945, 0.23846027, -0.083913095, -0.29872265, -0.040147282, -0.08981918, -0.04329814, -0.12339693, -0.034489952, 0.013393211) * go_2(1.0, 0.0); result += mat4(0.33091688, 0.1726297, 0.034332044, -0.091396205, 0.15434311, -0.0022870845, -0.15506189, 0.08710491, -0.16063525, 0.042252056, 0.017086457, 0.08134797, 0.08631321, 0.037843138, 0.088296555, 0.0064518084) * go_2(1.0, 1.0); result += mat4(0.09161051, 0.114355795, -0.15304486, -0.030537153, 0.1835368, -0.3287635, 0.031197926, 0.09717476, 0.04276852, 0.113250345, 0.05949038, -0.10599563, 0.43574792, -0.060788117, 0.18409383, 0.12678055) * go_3(-1.0, -1.0); result += mat4(-0.018356865, -0.0072578182, 0.12020777, -0.013127592, 0.20136636, -0.22984362, 0.06896224, 0.00044982752, 0.008428429, -0.123316936, -0.09989286, 0.078248784, -0.16313677, -0.003020313, -0.46285018, -0.08967125) * go_3(-1.0, 0.0); result += mat4(-0.03451497, -0.10864502, 0.13207638, 0.17194521, 0.0037514758, -0.20222199, -0.12535086, 0.001511977, 0.056294486, -0.2112898, 0.078261316, 0.10118746, -0.044742294, 0.21793383, -0.19927903, -0.21338293) * go_3(-1.0, 1.0); result += mat4(-0.034903776, -0.10167085, 0.031066334, 0.0379958, 0.20532596, -0.17457838, 0.16556816, -0.0021619152, 0.02682665, 0.03396325, -0.059273884, 0.1922813, -0.072151475, -0.010240544, 0.2302027, 0.12385962) * go_3(0.0, -1.0); result += mat4(-0.20170145, -0.08203941, -0.028107846, -0.18003726, 0.44744352, -0.13190243, 0.13233365, 0.03626546, 0.085763134, -0.25613126, -0.11213388, 0.15529087, -0.271649, 0.050587676, -0.062583975, 0.057289865) * go_3(0.0, 0.0); result += mat4(-0.040649455, -0.17949733, 0.35847965, -0.040587306, 0.24314344, -0.23811667, 0.13958354, 0.04961874, 0.09858903, -0.04202913, -0.21850993, 0.0700419, -0.09130745, -0.096835814, 0.0022782686, -0.25416258) * go_3(0.0, 1.0); result += mat4(-0.08215545, -0.019647893, 0.055263475, 0.053733055, 0.098485716, -0.1041945, -0.06541415, -0.08868577, -0.07262986, 0.03513784, -0.110529095, -0.03369232, 0.056786604, 0.2569229, -0.05931065, -0.22081214) * go_3(1.0, -1.0); result += mat4(0.066926084, 0.029664058, -0.10779271, 0.11026963, 0.23927264, -0.16914488, 0.022947345, 0.12303853, -0.07066212, -0.013205378, 0.15348643, 0.035568032, 0.20966691, 0.010149819, -0.08814468, -0.064854674) * go_3(1.0, 0.0); result += mat4(0.11493852, -0.074924305, -0.14840698, -0.16956823, 0.056806292, -0.06387947, -0.06880271, -0.04637334, -0.1929893, 0.18226422, 0.064644486, -0.1594863, 0.027403917, 0.13951495, -0.06569123, -0.07700207) * go_3(1.0, 1.0); result += vec4(-0.043347504, -0.20504741, -0.037821215, -0.014486937); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf1 //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.047881734, -0.09396414, -0.2839081, 0.3140853, 0.052613556, 0.09940423, 0.23960467, -0.022228222, -0.12065009, 0.07898222, 0.08657881, 0.010852739, -0.050450284, 0.01683982, 0.031813968, 0.053060856) * go_0(-1.0, -1.0); result += mat4(-0.10252411, -0.03116448, -0.30114275, -0.0316799, -0.017501019, -0.03006003, -0.2095696, 0.10134927, -0.3901916, -0.15335023, -0.11955071, 0.1337449, 0.101239376, -0.25044814, 0.2128469, 0.018979514) * go_0(-1.0, 0.0); result += mat4(-0.13392173, 0.052036732, 0.1682114, -0.026263753, 0.027221246, -0.15121374, 0.13723798, 0.08950682, -0.1182108, -0.07294226, 0.023392374, 0.052329235, -0.05632852, -0.07036173, 0.06872573, 0.05238042) * go_0(-1.0, 1.0); result += mat4(0.18112028, 0.18242362, -0.06812871, 0.032463413, 0.124638766, -0.26765212, -0.07678663, 0.33806562, 0.09674393, 0.15574542, 0.23634006, -0.02873782, -0.1626769, -0.14760062, -0.007274849, 0.09866139) * go_0(0.0, -1.0); result += mat4(-0.10726673, -0.10925056, 0.19967109, -0.19936769, 0.15942842, -0.14870064, 0.15493345, -0.08489036, -0.49053356, -0.17321263, 0.28426084, 0.18721215, -0.09898434, -0.2751838, -0.11833524, 0.028445128) * go_0(0.0, 0.0); result += mat4(-0.11788817, -0.23724948, -0.046072144, 0.035621114, 0.04527003, -0.0073492974, 0.11097195, 0.06806836, 0.04814677, -0.1408476, -0.1325629, 0.00929532, -0.16699041, -0.03034791, 0.08320368, -0.15429299) * go_0(0.0, 1.0); result += mat4(0.2729515, 0.008244692, -0.17441982, -0.39026466, 0.17381759, 0.31194404, 0.055934936, 0.20744409, 0.20119062, 0.0734271, 0.0796807, 0.0031037466, -0.0016392237, 0.033733975, 0.07149338, 0.042083208) * go_0(1.0, -1.0); result += mat4(0.07985744, 0.10945015, 0.018472541, 0.1397503, 0.2005682, 0.42641, 0.23022486, -0.2916921, 0.028285174, -0.31885162, -0.27070364, -0.10390779, 0.0751492, 0.12752363, -0.2279459, 0.08998453) * go_0(1.0, 0.0); result += mat4(0.18450491, -0.140783, -0.008006845, 0.09029298, 0.12536179, 0.26949662, 0.09491545, 0.063907005, 0.11212244, 0.09778506, -0.1835966, -0.053119674, 0.0072294096, 0.25018227, 0.010868525, -0.22721334) * go_0(1.0, 1.0); result += mat4(-0.028011927, -0.20073172, 0.5976166, -0.19494139, 0.17958745, -0.03838646, 0.058325976, -0.29409218, -0.12793432, 0.03245129, 0.35662368, -0.05048354, -0.13368197, -0.06151968, -0.012714591, -0.1763054) * go_1(-1.0, -1.0); result += mat4(0.18468465, 0.31682113, 0.12818255, -0.117110476, 0.13709468, -0.10034022, -0.07994527, -0.1259309, 0.04067299, -0.1147398, 0.28361055, 0.27916273, 0.03696692, 0.16829546, 0.27819383, 0.08305029) * go_1(-1.0, 0.0); result += mat4(-0.28920117, -0.033877946, 0.01586206, 0.04681198, 0.024248574, -0.045777842, -0.03342128, 0.07525412, -0.063377544, -0.016737273, 0.11235511, -0.04325238, -0.24170023, -0.09993599, -0.03205371, 0.14339828) * go_1(-1.0, 1.0); result += mat4(-0.008357902, -0.11038377, 0.03709221, 0.26775306, 0.07963845, -0.25377446, -0.17630441, -0.10966474, 0.057311732, -0.083327, 0.044497233, 0.06903858, -0.26531395, -0.103399664, -0.14806591, 0.269314) * go_1(0.0, -1.0); result += mat4(0.05450808, -0.041993964, -0.07217651, 0.034468375, 0.2117634, 0.0075620585, 0.05825411, -0.2252478, -0.0527787, 0.049732126, -0.032040413, -0.09361454, 0.29585132, 0.018413153, 0.18384546, -0.024226356) * go_1(0.0, 0.0); result += mat4(-0.031109914, 0.19351351, 0.07405522, -0.06313074, -0.09983541, -0.011495182, 0.11749038, -0.16775608, 0.2790974, -0.09338754, 0.07913264, 0.103792936, -0.18679164, -0.15639925, 0.112943865, 0.07930375) * go_1(0.0, 1.0); result += mat4(0.004106195, -0.036833283, 0.12908752, 0.12869535, -0.02472107, 0.17561707, -0.025890926, -0.18789047, 0.096218705, -0.16306408, -0.02198454, -0.010134957, -0.09710009, 0.002062143, -0.046785697, 0.0029441968) * go_1(1.0, -1.0); result += mat4(0.19648251, -0.015663045, -0.0730215, 0.028611008, 0.13529862, -0.015256192, -0.04119306, -0.24628192, 0.02601027, -0.21184283, -0.1962902, 0.09109358, -0.06792383, 0.092336476, 0.12215351, -0.08596062) * go_1(1.0, 0.0); result += mat4(-0.17530201, -0.0351919, -0.31872514, -0.13933206, -0.07000922, -0.049807087, 0.0010997375, -0.033573963, 0.07442056, -0.33290103, -0.40381998, 0.09435, -0.3280128, -0.09953127, -0.11283648, 0.20685865) * go_1(1.0, 1.0); result += mat4(-0.052573867, -0.035328753, -0.11132943, -0.17515652, 0.05021051, 0.058642425, -0.046640664, 0.0799107, -0.027398815, -0.33619994, -0.22135767, 0.07894002, -0.14941697, -0.0940996, -0.11655085, 0.049795926) * go_2(-1.0, -1.0); result += mat4(-0.039301276, 0.041062318, 0.20312686, -0.009338705, 0.013706282, -0.0245852, 0.03458311, 0.09601228, -0.18203016, -0.012260314, 0.17984508, -0.056576703, -0.102844186, 0.24047872, 0.05307189, 0.16066082) * go_2(-1.0, 0.0); result += mat4(0.1478775, 0.0046362123, 0.05459521, 0.07162838, -0.01896149, 0.23700175, -0.14174299, 0.06988599, -0.32545477, -0.08065096, -0.061227743, -0.0010796773, 0.094327345, -0.20760082, -0.19523263, 0.19859222) * go_2(-1.0, 1.0); result += mat4(-0.049676366, -0.10381536, 0.02546116, -0.13127093, 0.10954914, 0.0048147943, 0.06962328, -0.30456528, -0.11956627, 0.0150488885, -0.10711722, 0.1684613, -0.1939089, -0.10577047, -0.11980919, -0.036988296) * go_2(0.0, -1.0); result += mat4(-0.054795764, 0.09491116, -0.08494948, 0.059765853, 0.0131597435, 0.20786162, 0.11999637, 0.024381055, 0.22830428, 0.027053319, -0.011646274, -0.12145409, -0.07899559, -0.012688263, 0.10684157, 0.3824219) * go_2(0.0, 0.0); result += mat4(-0.23994572, -0.0031532666, -0.0050638164, 0.14236279, 0.05690383, -0.06259682, 0.052624144, 0.20461404, -0.19230312, -0.11072268, 0.013023965, 0.08931543, -0.21997221, 0.11760443, -0.40943825, 0.28656834) * go_2(0.0, 1.0); result += mat4(-0.06606179, 0.26007771, 0.033754125, 0.119690455, 0.024669139, -0.06752839, 0.12688096, -0.0063201943, -0.17123021, 0.07548857, -0.14213699, 0.034093797, -0.15632647, -0.123243414, -0.42634043, 0.1715022) * go_2(1.0, -1.0); result += mat4(-0.046503466, 0.13876389, 0.17973013, -0.25938338, -0.18824704, -0.11876702, 0.31065792, -0.041042212, -0.061369427, 0.2057992, 0.17295738, 0.3836555, -0.21109799, -0.10167118, 0.16577047, 0.113483034) * go_2(1.0, 0.0); result += mat4(-0.24534856, -0.014482421, 0.22515748, -0.12773542, 0.12794174, -0.02528619, 0.41710484, 0.09154934, -0.17805946, -0.25428918, 0.07294183, 0.047079418, -0.30949152, -0.08919157, 0.17888431, 0.17706038) * go_2(1.0, 1.0); result += mat4(-0.1741826, 0.046225294, -0.10761791, 0.2619953, 0.007373745, 0.05104337, -0.22309966, 0.34529984, -0.034363825, -0.022187237, -0.08609555, 0.16842419, 0.28136057, 0.17843607, -0.11307746, -0.05668021) * go_3(-1.0, -1.0); result += mat4(-0.12310616, -0.29661375, -0.10581025, -0.049584012, 0.19651765, 0.08436489, -0.14533581, -0.029874112, -0.15422897, -0.062741704, -0.22694711, -0.15547274, -0.15181333, 0.0286061, 0.022438493, -0.062447168) * go_3(-1.0, 0.0); result += mat4(0.3497046, -0.09455009, 0.060618952, -0.2134236, 0.054515295, 0.07451165, -0.09267233, -0.010513333, 0.13842636, 0.11563433, -0.054750167, 0.050432, 0.1514256, 0.04284002, -0.2095581, 0.07907657) * go_3(-1.0, 1.0); result += mat4(-0.11745651, -0.04717057, 0.085377194, -0.065956995, 0.07280491, 0.2730059, 0.11088276, 0.2437957, 0.14018989, 0.1164107, -0.09516929, 0.0022427947, 0.111544006, -0.0680495, 0.09324579, -0.12482022) * go_3(0.0, -1.0); result += mat4(-0.07995795, -0.03387884, 0.019846136, 0.10231208, -0.07017192, 0.18659039, 0.035161644, 0.101182766, -0.14901665, 0.21307294, 0.063894205, -0.27546507, -0.24792959, -0.067731075, 0.13146006, -0.19333683) * go_3(0.0, 0.0); result += mat4(0.034206454, 0.1472648, -0.07406727, 0.014654025, 0.18703444, 0.1319857, -0.10610886, 0.08427947, -0.017536618, -0.06487879, -0.12095286, -0.050414838, 0.03260879, 0.1558894, -0.031887084, 0.11840288) * go_3(0.0, 1.0); result += mat4(0.114811294, -0.14574333, -0.09392587, 0.042283528, 0.08919092, 0.18259068, 0.0980717, 0.21024778, -0.1280008, -0.027260462, -0.1129027, 0.18722472, 0.13733985, 0.047153983, 0.030871978, 0.1998385) * go_3(1.0, -1.0); result += mat4(-0.06783575, 0.004612595, 0.1153467, -0.11531557, -0.048889533, 0.07673577, -0.02041786, 0.22744459, -0.13092506, 0.13484807, 0.40003043, -0.053706612, -0.16985156, -0.04791236, -0.052443005, -0.08363625) * go_3(1.0, 0.0); result += mat4(0.18187882, 0.017893985, 0.17856054, 0.005413129, 0.014147176, 0.15102178, 0.12436294, -0.02176765, -0.16727823, -0.0364111, 0.17074408, 0.12899421, 0.31984514, -0.0072070034, 0.031895883, -0.1991405) * go_3(1.0, 1.0); result += vec4(-0.011865144, 0.11717201, -0.13823777, -0.059450272); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.082203194, 0.021720003, 0.03725474, -0.08048348, 0.2063248, -0.033020593, -0.17585336, 0.06476272, 0.012244563, 0.026554609, 0.014708393, 0.26606125, 0.14248778, 0.12817341, -0.039826933, -0.12751861) * go_0(-1.0, -1.0); result += mat4(0.24573852, 0.19695967, -0.06257417, -0.04782871, 0.3511875, -0.018083302, -0.077342674, 0.15247667, 0.20321761, -0.07479984, -0.09548503, 0.08109568, -0.23808748, 0.07246303, -0.004242619, 0.16162953) * go_0(-1.0, 0.0); result += mat4(0.13296306, 0.19495387, 0.009222276, 0.033592198, 0.20443891, 0.16063854, -0.2581601, -0.016132578, -0.2296461, -0.23647323, -0.15407176, -0.18265317, 0.2343241, -0.049697313, -0.09398783, 0.41931856) * go_0(-1.0, 1.0); result += mat4(-0.10866088, -0.40605694, -0.0042648134, 0.07943803, 0.26914695, 0.14816476, 0.037706107, -0.123223364, -0.19962949, -0.053534556, -0.08397409, -0.04244924, -0.075791344, 0.29629225, 0.2311928, 0.099177904) * go_0(0.0, -1.0); result += mat4(-0.1748319, -0.2003186, -0.32659066, -0.21007413, 0.20122464, 0.032196607, -0.026299698, 0.33395135, 0.11411664, 0.05971959, 0.09001304, -0.15936212, 0.012322024, 0.19936106, -0.411186, -0.08319479) * go_0(0.0, 0.0); result += mat4(-0.07349218, 0.006184436, 0.096199185, -0.050186496, 0.064047046, -0.03813128, -0.057007037, -0.025550695, -0.2863145, -0.008512981, -0.20615962, 0.18009211, 0.008298396, 0.22452813, 0.010843521, 0.20169461) * go_0(0.0, 1.0); result += mat4(0.2691149, 0.059546687, 0.08922005, 0.2252196, 0.30341956, -0.024489028, 0.087045394, -0.03856442, -0.14083561, -0.17683443, 0.14137806, 0.15520614, 0.2073925, -0.19525874, 0.23661858, 0.3098405) * go_0(1.0, -1.0); result += mat4(0.006530723, 0.04180736, -0.04762067, -0.064395495, 0.02396811, -0.13332283, 0.0037775645, 0.026309434, 0.0033065109, -0.08315753, 0.02917419, 0.12330464, 0.22819455, -0.07489677, 0.12829056, -0.097994626) * go_0(1.0, 0.0); result += mat4(-0.09983759, 0.032783493, 0.11085758, 0.08993078, -0.057110567, -0.018973934, -0.14946178, -0.03921629, 0.039757587, 0.015860094, 0.04989561, -0.19634786, 0.04351146, 0.019315343, 0.25972188, 0.17989321) * go_0(1.0, 1.0); result += mat4(-0.04111906, -0.165601, 0.0003682197, -0.056232415, -0.32716644, -0.24015541, -0.057547837, 0.05966729, 0.06854747, 0.03599213, -0.18798864, 0.1183447, 0.014268468, -0.1310834, 0.06415977, -0.19414157) * go_1(-1.0, -1.0); result += mat4(-0.00070661673, 0.17671427, 0.10584568, -0.060910843, -0.104282066, -0.22676118, -0.01907062, 0.24882245, -0.043454725, 0.07691623, -0.48371696, 0.013537671, -0.025488405, 0.061228953, 0.18548754, 0.028671112) * go_1(-1.0, 0.0); result += mat4(-0.0121596735, 0.09595702, -0.08244918, -0.1176173, 0.26773354, -0.021729136, 0.075465776, -0.0928876, 0.12461298, 0.16830076, -0.15302569, 0.113850676, 0.09811088, 0.13006307, 0.24999009, 0.10261325) * go_1(-1.0, 1.0); result += mat4(-0.032246377, 0.038265374, -0.26476422, -0.1442876, -0.19866082, 0.08649541, 0.041478764, 0.11155026, 0.21576422, -0.09572912, -0.11174068, -0.19722937, -0.15801935, 0.29604745, -0.08606268, -0.15532136) * go_1(0.0, -1.0); result += mat4(-0.06315591, 0.16151646, -0.009230362, -0.04341246, 0.09085519, 0.21924476, 0.38044852, 0.193819, 0.16622902, 0.0025134624, -0.22688466, -0.025276015, 0.07714917, 0.16302192, -0.11767101, -0.11086476) * go_1(0.0, 0.0); result += mat4(-0.04170153, 0.001859292, -0.26352355, 0.10982333, -0.031867817, 0.15773517, -0.060263418, 0.11117763, -0.017359972, 0.0127261225, 0.0782802, -0.16908924, 0.080516845, -0.05691526, -0.07530135, -0.14553802) * go_1(0.0, 1.0); result += mat4(0.06112685, -0.032287434, 0.17445667, -0.044935808, -0.11449107, -0.051394563, -0.029589338, -0.14555557, 0.03440661, 0.11035615, -0.17175, -0.14851089, 0.037362, -0.18740481, 0.17278154, 0.18073405) * go_1(1.0, -1.0); result += mat4(-0.27670652, 0.19484822, 0.2609349, 0.1455016, 0.04438468, 0.1449185, 0.11185832, -0.18598269, -0.019846648, 0.11886126, -0.098498635, 0.15737785, 0.011406795, -0.18860829, -0.13705735, 0.17535745) * go_1(1.0, 0.0); result += mat4(-0.30244905, -0.28695273, 0.1146976, 0.21144345, -0.037980128, -0.027679864, -0.13992494, -0.04884521, -0.032023884, -0.07921183, -0.16042095, -0.06935386, -0.06570237, -0.1107404, -0.018163798, 0.22625941) * go_1(1.0, 1.0); result += mat4(-0.07292955, -0.07321777, -0.045146503, -0.33291966, -0.096732594, -0.07203495, 0.33692798, 0.2870733, 0.122160144, -0.076574564, 0.042844944, 0.26448342, 0.07672146, -0.028775277, -0.12088313, 0.15583947) * go_2(-1.0, -1.0); result += mat4(0.21589327, 0.05258274, 0.09705794, -0.024653846, -0.039402515, 0.28485695, 0.14711736, -0.10556087, -0.15140481, 0.09039498, 0.017308712, 0.11862922, 0.08230978, 0.21678248, -0.043815188, -0.226433) * go_2(-1.0, 0.0); result += mat4(-0.029258793, 0.26618922, 0.02564014, -0.23189862, -0.24074338, -0.18556763, 0.25973624, 0.04746873, 0.0137007125, -0.22239363, -0.12414957, 0.048228756, -0.22406264, 0.282667, -0.021001073, -0.17465611) * go_2(-1.0, 1.0); result += mat4(0.32401654, -0.1495363, -0.20869227, 0.04271639, -0.0087802755, 0.031325378, 0.23834595, 0.039336167, 0.17265107, 0.20947595, 0.28737286, 0.0028783784, -0.057340365, -0.050347418, -0.11915604, -0.1831807) * go_2(0.0, -1.0); result += mat4(0.1811338, 0.07732653, 0.20975596, -0.47129005, 0.07121942, 0.08410583, 0.44170937, -0.19524159, -0.17807977, 0.12837476, 0.20816846, -0.1741958, -0.04411918, 0.06024972, 0.18159702, -0.052485272) * go_2(0.0, 0.0); result += mat4(-0.15229738, 0.27513, 0.28150418, -0.19543962, -0.02045864, -0.07207227, 0.09589587, 0.09110817, 0.061413247, 0.0046052113, 0.11619411, -0.2988938, 0.065739445, 0.10205611, 0.12847126, -0.028355654) * go_2(0.0, 1.0); result += mat4(0.0657154, -0.047568597, -0.16148911, 0.16392621, -0.25281775, -0.061153214, 0.017480455, -0.026288848, 0.20319715, 0.04763355, 0.010444491, -0.26671803, -0.25821987, 0.32863674, -0.30734694, -0.18190521) * go_2(1.0, -1.0); result += mat4(-0.042703815, 0.06633036, -0.048434302, -0.17176376, -0.12699759, -0.1124558, 0.083266065, 0.03354623, -0.13468939, 0.12706263, 0.053659134, -0.06930602, 0.008196115, 0.2034998, -0.06351442, -0.039730288) * go_2(1.0, 0.0); result += mat4(0.09614661, 0.22500272, 0.088511504, -0.16960482, 0.15364788, -0.18854137, -0.13163191, -0.07503735, -0.23177068, -0.0053305267, -0.041978605, 0.0971947, -0.049034655, 0.04486706, 0.09076307, -0.02310868) * go_2(1.0, 1.0); result += mat4(-0.1304683, 0.17743458, -0.09817326, -0.0646786, 0.07886976, 0.20109388, -0.034114968, -0.2029261, -0.03348398, 0.029337432, -0.07302782, -0.02240758, 0.030242773, -0.30032325, 0.02085572, -0.027314361) * go_3(-1.0, -1.0); result += mat4(-0.037377544, 0.026350772, -0.07430488, -0.114671774, -0.126935, -0.046512567, -0.033628833, -0.19018382, -0.041053895, -0.031206857, 0.08562848, -0.01875709, 0.21099389, -0.092511, 0.0073047103, -0.009811013) * go_3(-1.0, 0.0); result += mat4(0.11358029, 0.17468451, -0.12739041, -0.14332245, -0.22230148, 0.16862972, -0.04462456, 0.2469604, -0.008622369, 0.0081848325, -0.17032363, -0.16024362, 0.21178265, 0.037127133, 0.08559072, 0.11584694) * go_3(-1.0, 1.0); result += mat4(0.008993893, -0.08037705, 0.4426555, 0.15593371, 0.15273719, -0.03249998, 0.055109, -0.1512612, -0.037183985, 0.20825677, -0.08516227, -0.06664223, -0.10011001, -0.3505215, -0.17941694, 0.052089088) * go_3(0.0, -1.0); result += mat4(-0.109703645, -0.13505603, 0.1336451, 0.13118869, 0.010915504, 0.12748592, 0.21201555, -0.40841985, -0.11059143, 0.033772044, -0.039282143, 0.03095394, 0.10394723, -0.21343367, -0.10699851, -0.028351074) * go_3(0.0, 0.0); result += mat4(0.019704714, 0.06243651, 0.09896519, -0.17492259, 0.012675787, -0.004239029, 0.21319824, 0.069183126, -0.0071114586, 0.123431124, -0.24479835, 0.00723795, -0.045293927, 0.014101029, 0.15746681, 0.042405806) * go_3(0.0, 1.0); result += mat4(0.023828225, -0.0015190929, 0.1194638, 0.082163885, 0.10532113, 0.042044062, 0.02528007, 0.015175004, 0.026613194, 0.33525538, -0.1627064, -0.29887968, -0.197707, 0.038967777, -0.15811683, -0.106895216) * go_3(1.0, -1.0); result += mat4(0.044362027, -0.04946742, -0.14815849, -0.17660522, -0.034201477, -0.012243106, -0.050183997, 0.06407372, 0.039822515, 0.15880872, -0.0672721, -0.4081093, 0.019489579, -0.060278706, -0.015096743, -0.07799167) * go_3(1.0, 0.0); result += mat4(0.11861756, 0.27113584, -0.14107186, -0.10246008, -0.124051, -0.1627854, 0.10698585, 0.2846401, -0.061731786, 0.1724438, -0.12428688, -0.09986041, -0.034171514, -0.07100923, 0.041739646, -0.11308375) * go_3(1.0, 1.0); result += vec4(-0.02981662, -0.26338395, -0.011632586, 0.15063232); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf1 //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.17082009, 0.031344634, -0.06131912, 0.00887183, -0.01528174, 0.12943709, 0.24537678, 0.008178781, -0.312396, -0.023583878, 0.07827866, -0.1231261, 0.15081584, -0.18161978, -0.25179705, -0.036934935) * go_0(-1.0, -1.0); result += mat4(-0.05768411, 0.16785417, -0.1788644, -0.0067257965, 0.021445744, 0.10066516, -0.23864186, 0.1450302, 0.12892793, 0.19856106, -0.24444748, 0.16531628, -0.044425935, -0.02775357, 0.009059946, -0.12958384) * go_0(-1.0, 0.0); result += mat4(-0.025798557, -0.17238182, -0.34056288, -0.20921059, -0.03576266, 0.1476854, -0.06264234, 0.14452787, -0.04130045, -0.07275762, 0.034578666, 0.2914669, 0.20879944, 0.21359251, -0.048695553, 0.2638088) * go_0(-1.0, 1.0); result += mat4(-0.022791177, 0.4204545, 0.116855636, 0.20241925, -0.010444933, -0.14462502, 0.022550104, -0.24423064, -0.09417524, 0.045358784, -0.11405829, 0.035979558, -0.2283092, -0.06670842, -0.23852053, -0.22417003) * go_0(0.0, -1.0); result += mat4(-0.14526704, 0.040880535, 0.14076385, 0.07795045, -0.059177604, -0.13056375, -0.3373641, -0.19344307, -0.29891858, -0.32578763, -0.29061425, 0.1562214, -0.13578376, 0.36586633, 0.24936736, 0.054629393) * go_0(0.0, 0.0); result += mat4(-0.025790233, -0.13020341, -0.10084969, 0.15767297, -0.09738769, 0.04034404, 0.0038675873, 0.043515608, 0.16899958, -0.29117966, 0.03420067, 0.14432564, -0.10473084, 0.21014084, 0.07775908, -0.09303797) * go_0(0.0, 1.0); result += mat4(-0.07443987, -0.16225167, 0.036251917, 0.028432872, 0.03759333, 0.004027401, -0.033941846, 0.0019474924, 0.02357054, 0.30748722, 0.1652115, -0.17361522, 0.16905582, 0.08048018, -0.23639561, -0.029408466) * go_0(1.0, -1.0); result += mat4(0.0461233, -0.09346199, -0.07063276, -0.19447634, -0.049339604, -0.0032855074, -0.22661209, -0.0543389, 0.11924857, -0.21691081, -0.1645725, -0.0075736847, 0.018572787, -0.06552861, -0.01777661, -0.11651732) * go_0(1.0, 0.0); result += mat4(-0.06425901, 0.123392984, -0.16395192, -0.093448035, -0.029316641, 0.0986573, -0.23135012, 0.011170849, 0.00023920486, 0.15296175, 0.35453254, -0.05189021, 0.20708887, -0.103900835, 0.081992395, -0.21829562) * go_0(1.0, 1.0); result += mat4(-0.019074136, -0.1572586, 0.27919227, 0.09119617, 0.035954695, 0.2941489, 0.18262725, -0.055522963, -0.21364328, -0.1573611, 0.104966134, 0.08228523, 0.19945285, -0.0039229114, -0.1565048, 0.028975379) * go_1(-1.0, -1.0); result += mat4(-0.18501253, 0.006473006, 0.06637501, 0.04295065, 0.06411007, 0.1166344, -0.10060226, 0.46296063, -0.08600344, -0.03560105, 0.012215349, 0.017885283, 0.061346993, 0.17336361, 0.01935021, 0.20198092) * go_1(-1.0, 0.0); result += mat4(-0.04451627, -0.10372061, -0.13968691, 0.14479733, 0.1660607, 0.19334625, 0.0085214665, 0.28863636, -0.07600901, -0.014777084, 0.13209191, -0.09045013, 0.104893915, -0.04776884, -0.007936376, 0.104568765) * go_1(-1.0, 1.0); result += mat4(0.023751335, -0.108048, -0.050531313, 0.15916029, 0.13246661, 0.04644228, -0.09586482, -0.17222965, -0.22898191, -0.033484615, 0.078883134, -0.052609313, -0.2721741, 0.045986425, 0.13972299, -0.28923607) * go_1(0.0, -1.0); result += mat4(-0.23364568, -0.008875902, -0.40894926, 0.060443908, -0.2839635, -0.5270991, -0.2500865, 0.002020195, -0.24488612, -0.04982319, -0.009110353, -0.018023955, 0.06647274, -0.25225738, 0.26154432, -0.033934146) * go_1(0.0, 0.0); result += mat4(-0.1535129, -0.21257545, -0.16553773, 0.17471452, -0.06203719, 0.15238857, 0.18702018, 0.18572305, 0.07740396, -0.074217625, -0.072156586, -0.2183728, 0.00403749, 0.13750519, 0.30362993, 0.06550286) * go_1(0.0, 1.0); result += mat4(0.37164542, -0.1980723, -0.15659203, 0.19498909, 0.01748114, 0.011807152, -0.05424202, 0.11926474, 0.050406165, -0.12925303, -0.020280985, 0.08429331, 0.14769496, -0.077555746, -0.15216178, -0.27070466) * go_1(1.0, -1.0); result += mat4(0.35804263, 0.08539285, -0.14785156, -0.13532467, 0.058254432, 0.20448379, -0.006173341, 0.058168225, -0.21714899, -0.13472849, -0.09392532, -0.12753737, -0.097461835, -0.11419082, 0.09384189, 0.06414768) * go_1(1.0, 0.0); result += mat4(0.023494452, -0.22187226, -0.16694295, 0.0204334, -0.26720086, 0.15916729, 0.3098874, -0.10292057, 0.008854983, 0.13375004, -0.04409455, 0.09286524, 0.095829524, 0.12427317, -0.048659876, 0.18300754) * go_1(1.0, 1.0); result += mat4(-0.119153984, 0.10163183, 0.025017537, -0.40096784, 0.026778705, 0.15821172, -0.19947284, -0.33337715, 0.2952563, 0.16820388, -0.057061996, -0.029319009, -0.12184868, 0.09031512, 0.12028806, 0.021044692) * go_2(-1.0, -1.0); result += mat4(0.086744264, -0.046958666, 0.2130253, -0.46672252, 0.07135636, 0.0100029735, -0.13828261, -0.012365689, -0.11374441, 0.21084632, -0.059631422, -0.013799735, -0.037889663, -0.10701892, -0.09493782, 0.15516634) * go_2(-1.0, 0.0); result += mat4(0.031181194, -0.01535001, 0.029270316, 0.13128386, 0.11838377, -0.17051528, 0.12228499, -0.04841128, 0.33350074, -0.006144013, -0.09055018, 0.27470216, -0.26665646, -0.08703671, -0.01719071, -0.23449609) * go_2(-1.0, 1.0); result += mat4(-0.12856458, 0.005562174, -0.19517267, 0.13270985, 0.2776414, 0.032003902, -0.15778573, 0.15344355, 0.26930434, -0.13459459, 0.035019353, 0.08896612, 0.12847935, -0.122637205, 0.001815178, 0.08290523) * go_2(0.0, -1.0); result += mat4(0.33805037, -0.15318587, -0.20955376, -0.26121393, -0.026022578, -0.1617741, 0.1336867, 0.026223289, 0.012059392, -0.17295446, -0.060811974, 0.14027825, -0.21134059, -0.08408573, -0.23773228, 0.110836074) * go_2(0.0, 0.0); result += mat4(0.16176093, 0.15307428, -0.07711325, -0.3458805, 0.061291527, 0.023916256, 0.21370678, 0.0015756418, 0.10642374, 0.24807373, 0.11164451, 0.10780487, 0.087194376, -0.2718231, -0.008457387, 0.054078236) * go_2(0.0, 1.0); result += mat4(-0.03259038, -0.20923306, 0.165477, 0.098864526, -0.02734457, 0.08871225, -0.01552188, 0.047712058, 0.055032052, -0.13044262, -0.2899521, 0.22230095, -0.029343741, -0.16427459, -0.005436118, -0.05111821) * go_2(1.0, -1.0); result += mat4(0.20065974, -0.1556366, -0.12620135, 0.44572976, -0.020925352, 0.12025185, 0.20588058, 0.06391864, 0.046870507, 0.16942503, -0.049370963, 0.008779016, 0.04954915, 0.090298936, -0.16466027, 0.011152038) * go_2(1.0, 0.0); result += mat4(0.13587528, 0.047841422, 0.19804007, -0.1672396, -0.072491, 0.04543739, 0.25287256, 0.015226213, 0.02007356, -0.049578942, -0.08796175, 0.1714897, -0.07819061, 0.1509537, 0.093094915, 0.031139288) * go_2(1.0, 1.0); result += mat4(-0.013774682, 0.118201815, -0.009592314, -0.10837201, -0.0686881, -0.083380274, 0.107689425, 0.046642892, 0.119898744, -0.05502989, -0.19719897, 0.0005697584, -0.0921928, 0.032281205, 0.2568853, 0.2325449) * go_3(-1.0, -1.0); result += mat4(0.02991112, -0.09898633, 0.06076172, -0.20906185, 0.0026118348, 0.06130956, 0.06760944, -0.16662054, 0.065741204, -0.13144116, 0.011419801, 0.22552124, 0.1465757, -0.07417319, -0.10788749, -0.24952699) * go_3(-1.0, 0.0); result += mat4(-0.19238451, -0.024058497, 0.19580396, -0.067399554, -0.18832864, -0.11752747, -0.078949094, -0.23762032, -0.04141864, 0.022530237, -0.02222157, 0.0054874527, 0.057746816, -0.34854797, 0.028730657, -0.08976777) * go_3(-1.0, 1.0); result += mat4(0.16888975, 0.19949849, -0.08456147, -0.03619044, -0.019596824, 0.11214634, 0.13971676, 0.22926724, 0.03219445, -0.04566354, -0.14948955, -0.22817011, -0.08714846, -0.19684613, 0.15479128, 0.2433362) * go_3(0.0, -1.0); result += mat4(0.16050309, -0.102841675, 0.20855242, -0.011171905, -0.10309409, 0.22455123, 0.15892951, -0.06582373, 0.010079549, -0.2055006, -0.09385158, 0.006519388, 0.11838815, 0.37134558, -0.165772, 0.12704434) * go_3(0.0, 0.0); result += mat4(0.11643292, 0.03294274, -0.09800525, -0.13601723, -0.081318736, -0.059975546, -0.039105035, -0.2893635, -0.13024913, -0.058016162, -0.09961072, 0.10532414, 0.24250132, -0.35546342, -0.092634924, 0.093994915) * go_3(0.0, 1.0); result += mat4(-0.18799333, 0.25611782, 0.014645917, -0.063751906, 0.06498416, 0.16619027, -0.14411639, 0.3914421, -0.07343631, -0.116468735, -0.10941946, -0.2553544, -0.37774643, -0.0018441634, 0.06827239, -0.0122299045) * go_3(1.0, -1.0); result += mat4(-0.11884597, -0.2477297, 0.048488285, -0.06438257, -0.124703035, 0.25932777, 0.0650111, -0.0930877, 0.06463341, -0.000544085, 0.0147504965, -0.170097, -0.13241997, 0.20983136, -0.15956205, 0.03424298) * go_3(1.0, 0.0); result += mat4(-0.034574904, 0.06755256, 0.09508443, -0.17162292, 0.046379335, 0.2178781, 0.08699012, -0.055380464, -0.2237568, -0.07427848, -0.028395249, -0.3225617, -0.084454566, -0.24776657, 0.254169, 0.13229847) * go_3(1.0, 1.0); result += vec4(0.18765923, -0.07697714, 0.028134674, -0.060966115); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_7_tf //!WIDTH conv2d_6_tf.w //!HEIGHT conv2d_6_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.21919365, 0.36627784, 0.12603314, 0.24306288, 0.06447028, 0.06472204, -0.05997039, -0.15651788, 0.017059859, -0.006497198, -0.4189735, 0.021636713, -0.23887977, -0.014220949, 0.031113686, -0.17342716) * go_0(-1.0, -1.0); result += mat4(-0.10818789, -0.03273837, 0.33918005, -0.19290088, 0.0955361, -0.34107623, -0.054906327, -0.18083344, -0.060723677, 0.24395694, 0.112975016, -0.07254578, -0.14389384, 0.13235968, -0.15054801, -0.26216486) * go_0(-1.0, 0.0); result += mat4(-0.23442148, -0.07857079, 0.022283873, -0.2656417, 0.037092753, -0.037313666, -0.5057047, 0.042533103, -0.120424, 0.00021930189, -0.0044566668, -0.45536995, 0.00040759926, 0.14597592, -0.094990164, -0.036161344) * go_0(-1.0, 1.0); result += mat4(0.15024352, 0.19903262, -0.0734784, 0.092836305, -0.025753846, 0.024750374, -0.07550193, 0.035420835, 0.11084378, 0.26119822, -0.08443512, -0.0047807065, -0.042685136, 0.24889739, 0.098650105, 0.2088369) * go_0(0.0, -1.0); result += mat4(-0.25551823, 0.14455976, 0.19886157, -0.23465924, 0.20711218, -0.20875362, -0.11320392, -0.30852005, -0.06795657, 0.008670962, 0.30601278, 0.6929064, 0.17079145, 0.15744895, 0.06441601, 0.06514001) * go_0(0.0, 0.0); result += mat4(0.03142604, -0.006410137, -0.023654792, -0.05708553, 0.062985405, -0.077010594, 0.078804865, 0.050882503, 0.010274228, -0.15558401, 0.09490256, 0.14964707, -0.11966925, -0.36176664, 0.27809814, -0.18862294) * go_0(0.0, 1.0); result += mat4(0.05609992, 0.0041612233, -0.08498908, 0.04479823, -0.080117956, -0.17423204, -0.22858045, 0.054569032, -0.050866384, -0.020000307, 0.027000953, -0.67724514, 0.16240878, -0.04641204, 0.0648367, -0.20613132) * go_0(1.0, -1.0); result += mat4(0.08542306, -0.08254248, -0.11090553, -0.14140448, -0.10788511, -0.13011602, -0.29319742, -0.26007155, 0.11033401, -0.31966573, 0.32668245, 0.19542319, 0.06329418, 0.20904626, 0.2724067, -0.009155685) * go_0(1.0, 0.0); result += mat4(-0.007403411, 0.0012836396, -0.23446666, -0.03017208, 0.062420018, -0.13611084, -0.2975928, 0.13173148, -0.03679939, 0.13743873, -0.10121899, 0.074514665, 0.1497629, -0.09523838, 0.39018926, 0.37807035) * go_0(1.0, 1.0); result += mat4(0.11441487, -0.19565523, -0.25757137, -0.16148767, 0.15575317, -0.12657928, 0.10479676, 0.062919036, 0.010544159, 0.22931573, 0.20360178, 0.4637635, -0.3395036, -0.52467215, 0.08759308, 0.028030418) * go_1(-1.0, -1.0); result += mat4(0.2699195, -0.34218305, 0.15259695, 0.03139074, -0.024053533, -0.029567484, 0.28480124, 0.20525953, 0.15452823, -0.217713, 0.15861876, -0.012275699, 0.21408023, 0.097508304, -0.57126766, -0.14679857) * go_1(-1.0, 0.0); result += mat4(-0.0755847, -0.09751562, -0.29480466, -0.22285318, 0.14196442, 0.114573136, -0.22294767, 0.12463806, 0.3322209, -0.04631724, -0.11097061, -0.27986854, -0.16099304, -0.060079545, 0.00299308, 0.120776065) * go_1(-1.0, 1.0); result += mat4(0.050933484, -0.13776319, -0.18809728, 0.24035202, -0.32528606, -0.41684148, -0.029342847, 0.28642926, -0.07963454, -0.12905268, 0.07606093, 0.24670005, -0.08815598, -0.23320907, -0.008099349, 0.21512873) * go_1(0.0, -1.0); result += mat4(0.19247563, 0.18083979, -0.09719762, 0.15314941, -0.22350982, 0.46515045, -0.3571128, 0.35953265, 0.06921985, -0.4482386, -0.18732521, -0.5043983, 0.35159567, -0.33315298, -0.21884166, -0.16283798) * go_1(0.0, 0.0); result += mat4(-0.021124054, -0.007966742, 0.0052493825, 0.022550896, 0.030403977, 0.3377868, -0.47602004, -0.077664234, -0.07222509, -0.07486097, -0.37971064, -0.5107857, -0.06299477, 0.04930232, -0.3330487, 0.29845512) * go_1(0.0, 1.0); result += mat4(-0.063705474, -0.07917637, -0.02026607, -0.05142568, 0.021577014, -0.07379867, 0.033937998, 0.08148773, -0.02717838, -0.03233838, 0.098000035, 0.036476444, -0.13366953, 0.014477577, 0.24064232, 0.39313284) * go_1(1.0, -1.0); result += mat4(-0.16046515, -0.094624564, 0.35435164, 0.09942324, -0.07137174, -0.27999225, 0.124644354, -0.0062176553, 0.015016751, -0.05500243, -0.23249559, -0.4508382, 0.1860433, 0.10671491, -0.033345353, -0.06611453) * go_1(1.0, 0.0); result += mat4(0.21614046, -0.01307525, -0.18941112, -0.20533535, -0.14481686, -0.47801897, 0.22605121, -0.20298961, -0.06744227, -0.20377496, -0.11926173, 0.15645133, -0.31570885, -0.3495616, -0.024666889, 0.040965475) * go_1(1.0, 1.0); result += mat4(-0.11748018, -0.039976366, -0.00084064255, -0.028653437, -0.16216733, -0.036768105, 0.018064514, -0.0928936, 0.14008482, -0.064511225, 0.24329947, -0.0268608, 0.050330248, 0.08540601, -0.07272679, -0.01187671) * go_2(-1.0, -1.0); result += mat4(-0.09459936, -0.011723822, -0.06952858, -0.07808506, -0.065588176, 0.332501, -0.0120042395, 0.07668016, 0.14735217, -0.14856043, -0.06702449, -0.020953184, -0.023006834, 0.06135422, 0.1491448, -0.028061569) * go_2(-1.0, 0.0); result += mat4(0.25136968, 0.25146323, -0.108277924, -0.20407207, -0.0013780294, 0.16108194, 0.25143847, 0.06672421, -0.033905584, -0.021144686, -0.019152718, 0.34619498, 0.14560962, 0.034437314, 0.024790365, -0.049976267) * go_2(-1.0, 1.0); result += mat4(-0.24928351, 0.12637813, 0.23609994, 0.12722939, -0.036997862, -0.16554876, 0.11144095, -0.10040036, -0.020359103, -0.080701865, -0.3142192, 0.27257237, 0.13546956, -0.14416885, 0.028196262, -0.2886465) * go_2(0.0, -1.0); result += mat4(0.28524777, -0.4236231, 0.27420738, -0.21095508, 0.23475651, 0.115876295, -0.18837357, -0.0260708, 0.030670704, -0.11516913, -0.11365572, -0.2203149, -0.018612983, -0.10719593, -0.031727783, 0.1403327) * go_2(0.0, 0.0); result += mat4(0.07240512, 0.03139215, 0.12328737, -0.021201206, -0.13971715, 0.072742075, -0.0011289873, 0.0053133667, 0.035639685, -0.04322272, -0.19288473, -0.15812221, -0.19126481, 0.0698514, 0.17619178, -0.035605464) * go_2(0.0, 1.0); result += mat4(-0.18552057, 0.07259671, 0.011667668, -0.15630563, 0.11414356, 0.14482655, -0.04021029, 0.18495587, -0.11386139, -0.09058561, -0.011265998, 0.23358451, 0.0521358, 0.12495261, 0.021644838, -0.048094347) * go_2(1.0, -1.0); result += mat4(-0.09222373, 0.0533347, 0.055820454, 0.22382596, 0.18713981, 0.2668916, -0.019384036, 0.012698582, 0.13325234, 0.20361474, -0.33106443, -0.08571572, -0.21243028, -0.10996386, 0.123459645, 0.1534967) * go_2(1.0, 0.0); result += mat4(0.18133277, 0.18108074, -0.05638664, 0.29533157, -0.2108019, -0.033636626, 0.5015888, -0.15116066, -0.041320793, -0.14764231, 0.07314567, -0.18865979, 0.10276937, 0.094240844, -0.1364283, 0.27812913) * go_2(1.0, 1.0); result += mat4(0.06040915, 0.23753685, 0.19019844, 0.23948252, -0.07535012, 0.11848904, 0.14389765, 0.050067905, 0.16150077, -0.030053454, 0.12478255, 0.26020208, 0.111198805, 0.06787492, -0.12771018, 0.006687384) * go_3(-1.0, -1.0); result += mat4(-0.5421617, 0.10414128, -0.21526064, -0.08883624, 0.13145073, -0.29695904, 0.57386386, 0.073361695, -0.09538372, 0.27593842, 0.070922814, 0.21769938, 0.06214975, 0.11847816, 0.10033405, 0.29360098) * go_3(-1.0, 0.0); result += mat4(-0.16294672, -0.014815565, 0.22046989, 0.16858687, 0.058917344, 0.21384977, 0.18803519, 0.105688855, 0.0355118, 0.20571202, -0.07341922, 0.26624045, -0.0415102, 0.050942056, 0.19727907, 0.20122413) * go_3(-1.0, 1.0); result += mat4(-0.020470422, 0.15815964, -0.13437317, -0.1967045, 0.074902646, 0.08356444, 0.055913117, -0.12837863, -0.18647918, 0.07002247, 0.038864706, -0.07288784, 0.04135125, -0.016055549, -0.1340297, -0.15578008) * go_3(0.0, -1.0); result += mat4(-0.07685624, 0.00079105416, -0.068755336, 0.110282525, -0.014170752, 0.041282844, -0.17035173, 0.19439398, -0.3036256, 0.024148455, -0.19566648, -0.06736254, 0.14203559, -0.13016985, -0.32845357, -0.14266774) * go_3(0.0, 0.0); result += mat4(0.0087252045, 0.098839566, -0.08770506, -0.08499465, 0.015245115, -0.110854514, 0.054458305, -0.018121868, -0.09666134, -0.08316006, 0.24617113, -0.17195955, 0.2574254, 0.06734342, -0.13792352, -0.07306126) * go_3(0.0, 1.0); result += mat4(-0.0073954533, -0.20126835, -0.22545357, -0.29462856, 0.057408337, 0.11939119, -0.01846476, 0.12534486, 0.15751605, -0.14282645, -0.14219986, 0.14283386, 0.14090413, 0.10500912, 0.03039335, 0.17448832) * go_3(1.0, -1.0); result += mat4(0.043910783, -0.09140025, -0.21666165, 0.07616939, 0.104454786, 0.309926, -0.12906921, 0.1140117, 0.09372434, 0.049547072, -0.086615674, -0.034449168, 0.096705064, 0.26001686, 0.027063297, 0.12422948) * go_3(1.0, 0.0); result += mat4(0.1365422, 0.2679611, 0.12037257, 0.43346113, 0.08223084, -0.016788265, 0.13570398, -0.017974345, -0.17922844, -0.09475725, 0.073539585, -0.106947675, 0.08998511, 0.04133868, 0.16586913, -0.26291734) * go_3(1.0, 1.0); result += vec4(-0.19233678, 0.016725872, -0.008011114, -0.1977463); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_7_tf1 //!WIDTH conv2d_6_tf.w //!HEIGHT conv2d_6_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.36016628, 0.019064043, 0.3073228, 0.16891135, 0.026739368, 0.31136194, 0.11260383, -0.26918694, 0.0419928, -0.3365078, 0.20189743, -0.04136312, 0.039564647, 0.033199426, 0.18768296, -0.017119858) * go_0(-1.0, -1.0); result += mat4(0.28663483, -0.41716507, 0.059281543, 0.043736435, 0.0028875466, 0.13817391, -0.12543318, -0.2794053, -0.023528943, 0.10610115, 0.09100278, 0.040132936, -0.21949205, -0.027810011, -0.0301218, 0.084047124) * go_0(-1.0, 0.0); result += mat4(0.39674807, -0.0040878756, -0.038235947, 0.11880838, 0.009898328, 0.19107847, -0.009313831, -0.1554276, -0.047341663, 0.18049581, -0.029317195, 0.0708909, 0.0708316, -0.110617444, 0.14584038, -0.022261223) * go_0(-1.0, 1.0); result += mat4(-0.20400241, 0.0896492, -0.010386381, -0.052133385, 0.005023956, -0.06628705, -0.16436209, -0.25345984, -0.05285192, 0.09706557, -0.03778914, -0.152546, 0.17023252, 0.063713826, 0.00743037, 0.056634087) * go_0(0.0, -1.0); result += mat4(-0.080793336, 0.4204207, 0.19098237, 0.20028038, -0.054076545, 0.22064368, -0.25853387, -0.3643562, 0.2085573, -0.023731, -0.06727709, -0.18683033, -0.18032159, -0.06388348, 0.304463, -0.2517781) * go_0(0.0, 0.0); result += mat4(0.11940941, 0.10624008, 0.16120581, 0.2369602, 0.3321827, 0.4272075, -0.10403669, -0.31388018, -0.006372124, -0.00653671, 0.109810196, 0.2277172, 0.005771998, 0.086026914, -0.08934813, -0.094941735) * go_0(0.0, 1.0); result += mat4(-0.13233568, 0.24112508, -0.0068006413, 0.12466225, 0.11396591, -0.07249253, -0.29090378, -0.12828146, -0.22001141, -0.08532405, -0.11932601, 0.29452974, 0.09572195, 0.017603843, 0.12454017, 0.16321751) * go_0(1.0, -1.0); result += mat4(0.042107448, -0.00807216, 0.06580674, -0.1289527, 0.13977426, -0.037159685, -0.21001346, -0.08698161, 0.22370502, -0.29170328, 0.2179206, 0.36621302, 0.0825477, -0.016513655, -0.11157249, 0.12861598) * go_0(1.0, 0.0); result += mat4(0.2246826, -0.13262233, 0.12131653, -0.15522355, 0.38104856, 0.030237729, 0.1286289, -0.19770473, -0.16175011, -0.13688888, 0.23505463, 0.21333031, 0.76352316, -0.17949077, -0.13124311, 0.1613879) * go_0(1.0, 1.0); result += mat4(-0.050607495, 0.0846705, -0.06136092, -0.033436477, 0.41138348, 0.037043408, -0.02676336, -0.37771952, 0.22147503, 0.06490757, -0.04266158, -0.22606373, 0.045775007, -0.054498192, -0.21495876, -0.036050417) * go_1(-1.0, -1.0); result += mat4(-0.06242522, 0.2700824, -0.05602621, -0.12361551, 0.14477442, 0.19403581, 0.23505251, -0.072234035, -0.15831544, 0.4640447, -0.104754634, -0.004539681, -0.20246096, 0.23216484, -0.35886365, 0.11360777) * go_1(-1.0, 0.0); result += mat4(0.14777757, 0.18951412, 0.027219458, 0.11216015, 0.02997997, -0.13466355, -0.0010830094, 0.021302953, 0.23441231, -0.14529245, 0.08068729, 0.10044398, 0.3972878, 0.26570204, 0.0046810666, -0.2863261) * go_1(-1.0, 1.0); result += mat4(-0.10385485, 0.1053724, 0.16961229, 0.20727012, -0.025148917, -0.011365095, 0.03899919, -0.030950211, 0.079080455, -0.32767853, 0.064670205, -0.035771385, 0.16833797, -0.21567492, 0.30871257, -0.19965471) * go_1(0.0, -1.0); result += mat4(-0.23420888, -0.004894698, -0.18162623, -0.31107524, 0.11976508, 0.14924951, -0.08723316, 0.21401922, -0.58200324, -0.01177345, -0.049033508, 0.19593577, -0.21139073, 0.13016601, 0.08734843, 0.4158892) * go_1(0.0, 0.0); result += mat4(0.0009789813, 0.33274913, 0.017405733, -0.042906318, -0.26410276, -0.09291333, 0.019387102, 0.105381854, -0.009176527, 0.09483514, -0.28462934, -0.03644404, 0.285194, -0.4260311, 0.14902237, -0.115670316) * go_1(0.0, 1.0); result += mat4(-0.09344311, 0.4463103, 0.19984834, -0.09733857, -0.118717775, -0.0708026, 0.24919955, -0.11234634, 0.1246395, -0.052909933, 0.1525815, 0.07724016, 0.0070534665, -0.06404165, -0.18149726, -0.014058336) * go_1(1.0, -1.0); result += mat4(-0.17353044, 0.15376104, 0.004588994, -0.13554202, -0.19920237, -0.18918681, 0.11327512, -0.117296435, -0.0785251, 0.013677155, -0.2103214, 0.06843426, -0.27790928, 0.09837545, -0.00019213746, 0.09132539) * go_1(1.0, 0.0); result += mat4(-0.01586651, 0.014929441, 0.2426186, -0.1889374, -0.0865462, -0.07454513, -0.20797268, -0.22366855, 0.19704159, 0.0048206006, -0.16707218, -0.14162683, 0.036798395, -0.1663155, -0.12009389, 0.09603803) * go_1(1.0, 1.0); result += mat4(-0.041532192, 0.05753804, 0.17927068, -0.042112097, 0.12080969, -0.15052572, -0.34855765, -0.07356988, -0.28199884, -0.18958664, 0.15879883, 0.08511588, 0.0034213227, -0.05338495, -0.37285298, 0.06626709) * go_2(-1.0, -1.0); result += mat4(-0.20219134, 0.22150375, -0.29405454, 0.06597703, -0.018885285, -0.010551704, -0.010774283, 0.08758955, -0.2015349, -0.17006227, -0.24321876, -0.06864207, -0.118437864, -0.043977212, -0.029736811, 0.14040919) * go_2(-1.0, 0.0); result += mat4(-0.18709077, -0.09723938, 0.12783436, -0.15167634, 0.29039705, -0.11009911, 0.018371418, -0.060096707, -0.07256923, -0.25799567, -0.06276934, -0.035992302, -0.06729111, -0.059956793, -0.024079734, 0.011838878) * go_2(-1.0, 1.0); result += mat4(0.010449175, -0.08212451, 0.1409803, 0.11861122, -0.18035835, 0.051930565, 0.01049551, -0.09447962, 0.12029649, 0.040604513, -0.059971705, -0.0044667358, -0.22080486, -0.11187681, 0.124374695, -0.004155485) * go_2(0.0, -1.0); result += mat4(-0.28584236, -0.38480133, -0.13987814, -0.4463469, -0.3890419, -0.022498172, 0.17334452, 0.21895568, -0.15450422, -0.10905497, 0.15111905, -0.22554915, 0.106121585, -0.029144369, 0.36059046, 0.22140682) * go_2(0.0, 0.0); result += mat4(-0.23780307, -0.023033705, 0.068205886, -0.110635854, -0.26720005, -0.1608183, 0.19523881, 0.07972837, -0.018495852, -0.2793956, 0.17668398, -0.12020479, -0.079556085, -0.02284952, 0.031480275, 0.31818348) * go_2(0.0, 1.0); result += mat4(0.22501226, -0.00829407, 0.059581667, 0.16512989, 0.18711442, 0.1200968, 0.11812652, -0.16091056, 0.15733972, 0.045156084, 0.20640492, -0.16852027, -0.11217177, 0.06746273, -0.050218176, 0.08643783) * go_2(1.0, -1.0); result += mat4(0.20715691, -0.1082907, 0.027892975, 0.19515261, -0.17838904, 0.1532257, -0.108409844, -0.06632365, -0.13805026, 0.23020233, 0.12416581, -0.14861803, 0.16650471, 0.08158386, -0.09051303, -0.06981649) * go_2(1.0, 0.0); result += mat4(-0.04617126, 0.06579221, 0.25964734, 0.28500968, 0.07641255, -0.090885855, -0.0972522, 0.18298368, -0.06393334, 0.103463, -0.23062052, -0.15270731, 0.13633437, 0.074707486, 0.15065335, -0.024602572) * go_2(1.0, 1.0); result += mat4(0.118319295, 0.010410938, 0.044655934, -0.104725905, 0.030477569, 0.12867387, 0.039075315, 0.18922117, 0.13301082, -0.1601557, 0.038168408, -0.07372259, -0.09522213, -0.095107146, -0.16679631, 0.044673234) * go_3(-1.0, -1.0); result += mat4(0.46229, -0.30780822, -0.09081465, 0.1433387, -0.0315039, 0.059409115, -0.24948491, -0.17146957, 0.060843736, -0.041989822, 0.054005735, 0.22835566, 0.12036598, -0.0070898845, 0.17276852, -0.17754094) * go_3(-1.0, 0.0); result += mat4(-0.35119572, 0.020034311, 0.08751943, 0.08193488, 0.041884877, 0.22649358, -0.07447533, 0.20845473, -0.04859846, -0.16206735, 0.06819576, -0.053000778, 0.18146423, 0.04694148, 0.045293212, 0.06783575) * go_3(-1.0, 1.0); result += mat4(0.280914, -0.14998704, -0.23485807, -0.015608296, 0.1549556, -0.11992663, -0.094974115, 0.05887284, 0.053392075, 0.10322464, -0.075066686, 0.068358354, -0.18663338, 0.009901499, -0.123370335, -0.12502703) * go_3(0.0, -1.0); result += mat4(0.7748568, -0.17870626, -0.20770052, 0.024692526, -0.056430295, -0.06324113, -0.03660047, 0.29629672, -0.51896983, -0.027231261, 0.05903762, 0.077677645, -0.061675485, -0.20277846, 0.10352223, -0.08198446) * go_3(0.0, 0.0); result += mat4(-0.06347568, 0.21643166, -0.09718546, 0.0372257, -0.029537952, -0.0357135, -0.09548363, 0.18225233, -0.29609334, -0.3496132, 0.18245913, -0.10162589, -0.18189451, -0.09077887, 0.117313184, -0.06863874) * go_3(0.0, 1.0); result += mat4(-0.047373574, -0.020289376, -0.25748715, -0.13568166, 0.15656634, -0.06841899, 0.012100781, -0.13611819, 0.0016357322, -0.23870537, 0.14035743, -0.14700134, 0.2535575, -0.13697346, -0.13693139, -0.10365287) * go_3(1.0, -1.0); result += mat4(0.4283934, -0.316192, -0.012617617, 0.018468965, 0.21436644, 0.18408814, -0.42651537, 0.12504087, -0.13894933, 0.091662176, -0.20096369, -0.080727175, -0.005487846, 0.17046383, 0.1383948, -0.0054956395) * go_3(1.0, 0.0); result += mat4(0.20014295, -0.027282396, -0.06317007, 0.04452042, 0.064600386, 0.072222926, -0.33409226, 0.08063831, -0.022607977, 0.1308856, -0.39691743, -0.094889864, -0.1810531, 0.011367248, -0.2531222, -0.22468317) * go_3(1.0, 1.0); result += vec4(0.26886886, 0.05874665, 0.10268232, 0.05833081); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-3x1x1x112 //!HOOK MAIN //!BIND MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!BIND conv2d_7_tf //!BIND conv2d_7_tf1 //!SAVE MAIN //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h #define g_0 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_1 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_5 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_8 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_9 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_12 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_13 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_16 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_17 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_20 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_21 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_24 (max((conv2d_7_tf_tex(conv2d_7_tf_pos)), 0.0)) #define g_25 (max((conv2d_7_tf1_tex(conv2d_7_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_7_tf_tex(conv2d_7_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_7_tf1_tex(conv2d_7_tf1_pos)), 0.0)) vec4 hook() { vec4 result = mat4(0.09689336, 0.06046458, 0.072598994, 0.0, 0.11994565, 0.104477674, 0.09302802, 0.0, -0.05718302, 0.050438102, 0.08814741, 0.0, 0.0308889, 0.0033925986, -0.01715605, 0.0) * g_0; result += mat4(-0.028314235, 0.06597744, 0.0966897, 0.0, 0.035656154, 0.07770106, 0.075551905, 0.0, 0.0001793458, -0.000479495, -0.00297406, 0.0, -0.053916585, -0.016807461, -0.0057141334, 0.0) * g_1; result += mat4(-0.047189303, -0.0207, -0.020910334, 0.0, -0.07933196, -0.06961211, -0.086069845, 0.0, 0.0943727, 0.008463375, 0.010755166, 0.0, 0.062410597, 0.022625161, 0.04068433, 0.0) * g_2; result += mat4(0.10270994, -0.019080428, 0.0050091282, 0.0, -0.004672948, -0.013966742, -0.0063746064, 0.0, -2.5856789e-05, 0.03151499, -0.0023983798, 0.0, 0.113539025, 0.12381699, 0.100360274, 0.0) * g_3; result += mat4(0.07868885, -0.030913834, -0.009213676, 0.0, 0.04870991, 0.021467991, 0.038739506, 0.0, -0.042969644, -0.07122453, -0.08798675, 0.0, -0.09784122, 0.021434791, 0.02510374, 0.0) * g_4; result += mat4(0.050420716, 0.0729716, 0.076532185, 0.0, -0.019112485, -0.01037939, -0.026948035, 0.0, -0.02591423, 0.008927897, -0.00042541025, 0.0, 0.1043701, -0.0071186824, -0.041817162, 0.0) * g_5; result += mat4(-0.16143242, -0.0009298223, -0.01228508, 0.0, 0.07744052, -0.018313263, -0.0488145, 0.0, 0.09241393, 0.07128674, 0.055164956, 0.0, 0.054884013, -0.04834418, -0.06281626, 0.0) * g_6; result += mat4(-0.049036566, -0.05979936, -0.05594288, 0.0, -0.014564307, 0.031926468, 0.037857566, 0.0, 0.015474487, -0.11385003, -0.11527764, 0.0, -0.07076006, 0.057038613, 0.095983796, 0.0) * g_7; result += mat4(0.03094887, -0.008734403, 0.00042712069, 0.0, 0.053891554, 0.05837673, 0.06200635, 0.0, 0.09071558, -0.04202184, -0.046172567, 0.0, -0.0425916, 0.04905093, 0.020835675, 0.0) * g_8; result += mat4(0.096628904, -0.037792254, -0.043241944, 0.0, -0.011923947, -0.025950424, -0.031381752, 0.0, -0.060941868, -0.07859433, -0.07535451, 0.0, -0.026777223, 0.08604982, 0.07829908, 0.0) * g_9; result += mat4(-0.06435972, 0.0036599538, 0.00786578, 0.0, -0.061972067, -0.05681472, -0.06667608, 0.0, -0.106890626, 0.007406496, 0.029977169, 0.0, -0.20519382, -0.044860814, 0.0021225857, 0.0) * g_10; result += mat4(-0.16876474, 0.012789643, 0.026692612, 0.0, 0.017817136, 0.026935097, 0.02227043, 0.0, 0.01690181, 0.07716103, 0.086527, 0.0, 0.07923805, -0.10443151, -0.10859543, 0.0) * g_11; result += mat4(0.003730466, -0.024648283, -0.022169832, 0.0, -0.0062762927, 0.022062732, 0.032966793, 0.0, 0.016349113, 0.017197203, 0.020952817, 0.0, -0.1763789, 0.035497356, 0.053835396, 0.0) * g_12; result += mat4(0.020886675, -0.07054202, -0.079142675, 0.0, 0.06664387, 0.044960167, 0.042230908, 0.0, -0.095019594, 0.012421141, 0.0142890485, 0.0, 0.056814816, -0.012751135, -0.014684506, 0.0) * g_13; result += mat4(0.011765893, 0.0008920681, -0.0018258415, 0.0, -0.010473814, -0.023085753, -0.028783914, 0.0, -0.023034256, -0.0024786016, -0.0052162083, 0.0, 0.1643386, -0.06132718, -0.09289065, 0.0) * g_14; result += mat4(0.016597198, 0.09389637, 0.10833379, 0.0, -0.043163072, -0.04714812, -0.035274632, 0.0, 0.09634976, -0.009292612, -0.022424143, 0.0, -0.08765172, 0.0051558353, 0.010900356, 0.0) * g_15; result += mat4(0.030815786, 0.021069322, 0.01812191, 0.0, 0.084839165, -0.0080813095, -0.029270556, 0.0, -0.10456346, 0.062386703, 0.0665605, 0.0, 0.11926609, -0.1104228, -0.13291118, 0.0) * g_16; result += mat4(-0.07159541, -0.007267032, -0.010134558, 0.0, 0.008234213, 0.045609634, 0.040295456, 0.0, 0.018416971, 0.01308482, 0.014649557, 0.0, 0.035107512, -0.02140815, -0.030279048, 0.0) * g_17; result += mat4(0.01918586, 0.03875863, 0.03229402, 0.0, -0.07917104, 0.041135103, 0.057182517, 0.0, 0.08609541, 0.0079662455, 0.004327576, 0.0, -0.14332893, 0.03120354, 0.056732506, 0.0) * g_18; result += mat4(0.03200192, -0.0035752193, -0.0031064528, 0.0, -0.010902813, 0.014607456, 0.019431474, 0.0, -0.016461229, -0.004938204, -0.004655488, 0.0, -0.033470232, 0.0026075812, 0.005896968, 0.0) * g_19; result += mat4(0.037410006, 0.048742272, 0.04348088, 0.0, 0.037719514, 0.030768529, 0.03127472, 0.0, 0.056426726, 0.03066893, 0.016440205, 0.0, -0.010599352, 0.022832409, 0.023211194, 0.0) * g_20; result += mat4(-0.005733291, 0.06365659, 0.06663611, 0.0, -0.041917093, -0.016493445, -0.020438088, 0.0, -0.0014357592, -0.0022506563, -0.0045095007, 0.0, 0.029893145, -0.009129354, -0.015173116, 0.0) * g_21; result += mat4(0.013052085, 0.005108175, 0.0025906067, 0.0, -0.021950055, -0.036447693, -0.036141638, 0.0, -0.036296472, 0.0068928464, 0.013102313, 0.0, 0.0060471976, -0.024798103, -0.023548538, 0.0) * g_22; result += mat4(0.0067743887, -0.06191211, -0.062355213, 0.0, 0.0016080744, -0.020445071, -0.016840393, 0.0, 0.028264903, 0.01852915, 0.015891539, 0.0, -0.023877412, -0.013271666, -0.008158679, 0.0) * g_23; result += mat4(-0.04317466, -0.018953001, -0.020452993, 0.0, -0.009322576, -0.03022352, -0.030970376, 0.0, 0.05653658, 0.05430553, 0.046692245, 0.0, 0.05615359, 0.059338935, 0.056018773, 0.0) * g_24; result += mat4(0.022878079, 0.03392234, 0.033057988, 0.0, -0.017554542, -0.0141542535, -0.014122613, 0.0, -0.048634093, -0.05316463, -0.047988772, 0.0, -0.058002178, -0.040221967, -0.034025013, 0.0) * g_25; result += mat4(-0.018253656, -0.04197674, -0.040467236, 0.0, -0.04358929, -0.028309818, -0.025425073, 0.0, -0.008488672, -0.001727991, 0.00035808363, 0.0, -0.0011709273, 0.0052514165, 0.0059479307, 0.0) * g_26; result += mat4(-0.08333935, -0.09818201, -0.09476284, 0.0, -0.033692095, -0.046259012, -0.045797516, 0.0, -0.007577072, 0.0022402718, 0.0016200038, 0.0, 0.0029786075, -0.020728534, -0.018938033, 0.0) * g_27; result += vec4(0.047567394, -0.02504617, -0.028163986, 0.0); return result + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Upscale_CNN_x2_M.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(-0.010995803, 0.077095956, -0.043992598, 0.06048717, 0.1164834, -0.11689607, 0.072985925, -0.078805886, 0.01182932, 0.054985743, -0.09018186, 0.044907484, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(0.1813623, -0.14752422, 0.025720436, -0.17639883, 0.15697388, 0.10445984, -0.1843076, 0.5264643, 0.047516696, -0.097305484, 0.09740847, -0.29619336, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(-0.014534763, 0.09486465, 0.046173926, 0.039391946, 0.09609376, -0.060574662, 0.042200956, -0.3269777, 0.051006425, 0.059818447, 0.04366627, 0.17699827, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(0.04268535, -0.08152529, 0.10577459, -0.036936995, -0.051562306, 0.054872766, 0.09194519, 0.0025066638, -0.01073954, 0.00064474024, 0.10038221, 0.02131141, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(-0.51751363, -0.40028602, 0.3469574, 0.5933738, -0.91357684, -0.67692596, 0.57815677, 0.39809322, -0.16341521, -0.27169713, 0.12232366, 0.4318641, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(0.12601124, -0.06263236, -0.45907676, -0.41514075, 0.3330334, -0.1929565, -0.6333532, -0.6552794, -0.045809917, 0.046351526, -0.26173338, -0.30252662, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(0.0030332592, 0.012103107, 0.010537323, -0.02038607, 0.095558085, 0.097704545, 0.083433494, 0.026790185, 0.01943357, -0.061712462, -0.00015703632, -0.032268334, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(0.016870102, 0.5215812, -0.11525501, 0.027527615, -0.09045733, 0.61310345, -0.1575268, 0.1905386, 0.020172214, 0.3503187, -0.08209157, -0.051328037, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(0.005494087, -0.010656317, 0.07682753, -0.08116042, -0.03934524, 0.16589017, 0.101483546, -0.066603065, 0.03494657, -0.07885597, 0.074227594, 0.0016264897, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(0.014463938, -0.0031906287, 0.007015422, -0.003888468); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.08532478, -0.14302494, -0.017921071, -0.0032664281, -0.09841952, 0.024187077, 0.10701477, 0.14110753, -0.05714981, -0.10897174, 0.073803626, 0.103992954, 0.07914382, 0.032193683, -0.18346278, -0.09723936) * go_0(-1.0, -1.0); result += mat4(-0.034482613, -0.10742312, -0.047286414, -0.08641124, -0.33896688, -0.036533825, -0.48337597, 0.034040943, -0.13598205, -0.080917805, 0.08540263, -0.012667689, -0.009171425, -0.120026454, -0.20536867, -0.032149274) * go_0(-1.0, 0.0); result += mat4(0.18687321, 0.066278316, 0.024327392, 0.08816582, -0.08017908, 0.09488853, 0.26018232, -0.101504356, 0.17487666, 0.31057635, 0.14785016, -0.09622089, -0.07537452, -0.13844088, -0.05810814, 0.09907489) * go_0(-1.0, 1.0); result += mat4(-0.04183032, 0.15207712, 0.005002397, 0.32277516, -0.16169126, -0.119836345, -0.04068436, -0.096728764, 0.11943901, 0.1789597, -0.20412198, 0.19009817, 0.36630696, 0.06946421, -0.5254373, -0.11896399) * go_0(0.0, -1.0); result += mat4(-0.31916487, -0.98911583, 1.0728644, -0.39280394, 0.33458877, -0.17325239, -0.645045, -0.28524077, -0.14512783, 0.24996442, -0.09837877, 0.05468934, 0.31559715, -0.020504637, -0.026724018, 0.24507573) * go_0(0.0, 0.0); result += mat4(-0.23759829, -0.08530173, -0.16665787, -0.22463752, 0.109896734, 0.13446991, -0.049552456, -0.02385489, -0.01245375, 0.3833208, 0.05758832, 0.1528937, 0.0501858, -0.19651426, 0.0076587177, -0.03297025) * go_0(0.0, 1.0); result += mat4(0.14554465, -0.01826686, 0.10284085, -0.19152659, -0.017585073, -0.05511482, 0.06362406, 0.023924058, -0.0018977845, -0.103172876, 0.03287086, -0.20085956, 0.36062446, 0.10749464, -0.20984372, 0.018256644) * go_0(1.0, -1.0); result += mat4(-0.005534592, 0.3709197, -0.18287498, 0.1720451, 0.030155553, -0.023265475, 0.0058617783, -0.031765483, 0.037328955, -0.2730994, 0.35090837, -0.3269043, -0.028477207, 0.32756507, -0.15989502, 0.12158258) * go_0(1.0, 0.0); result += mat4(0.10873739, 0.19583772, 0.060394943, 0.09410379, -0.04739245, 0.026561242, 0.022990001, 0.1093272, -0.01071349, -0.022938967, -0.046423864, 0.2385325, -0.0319821, 0.046962265, 0.09081178, -0.11001857) * go_0(1.0, 1.0); result += mat4(0.13012704, 0.112289295, 0.030790284, -0.050499484, 0.11784853, 0.08107028, -0.07556717, -0.15643, 0.015249331, 0.015299608, 0.07748125, 0.054485757, 0.044857923, 0.12161275, -0.048292994, -0.033995003) * go_1(-1.0, -1.0); result += mat4(0.12931514, 0.15114146, 0.070513315, 0.11246343, 0.4142387, 0.213479, -0.5439916, 0.07776645, 0.13109331, 0.2021147, 0.25932786, -0.22157331, 0.02377734, -0.014970623, -0.1943276, 0.18440372) * go_1(-1.0, 0.0); result += mat4(-0.22365458, -0.19829084, -0.06881161, -0.06468993, 0.17202774, 0.0048758537, -0.09235021, 0.18941896, 0.064125344, -0.09067088, 0.09748182, 0.13561936, -0.05876288, -0.0122420965, -0.054380875, -0.17743628) * go_1(-1.0, 1.0); result += mat4(0.18582906, -0.09263032, -0.08210888, -0.20515606, 0.11484005, 0.08557595, 0.0009253741, -0.051202174, -0.18535301, -0.1529345, -0.13092944, 0.03770747, -0.020947013, 0.19187425, -0.15494856, -0.048979875) * go_1(0.0, -1.0); result += mat4(-0.38131633, 0.4278787, 0.19763695, 0.27655518, -0.08711912, 0.07374453, -0.064803004, 0.5983854, 0.2361923, -0.057221692, -0.37138999, -0.24259573, 0.13890724, 0.25706333, -0.54021406, 0.08095518) * go_1(0.0, 0.0); result += mat4(0.0991328, -0.022651536, -0.029148921, -0.009812537, -0.09523686, -0.15704902, 0.052389514, 0.21561539, 0.1950314, -0.08572602, 0.0016523858, 0.14125621, -0.030999828, 0.12009709, 0.0373512, -0.105043754) * go_1(0.0, 1.0); result += mat4(-0.11251988, 0.12106985, 0.011923068, 0.3662747, 0.004800994, 0.017972551, 0.004761366, -0.07934206, -0.13755941, -0.022852683, 0.1502225, 0.009758547, -0.16964264, 0.00984782, 0.07855833, 0.035730787) * go_1(1.0, -1.0); result += mat4(0.01964957, -0.27226487, 0.033933397, -0.117632054, -0.009058229, 0.047830686, -0.01125145, 0.136628, 0.0056388285, 0.3028781, -0.12286517, 0.23498532, -0.009319075, -0.444048, 0.16174883, -0.06367683) * go_1(1.0, 0.0); result += mat4(0.02343933, -0.010915871, -0.058680378, -0.21886891, -0.010750894, -0.06671997, 0.0602906, -0.07903071, 0.066891186, 0.06650588, 0.14362891, -0.101870626, 0.02264628, -0.06940821, -0.077616625, 0.110911585) * go_1(1.0, 1.0); result += vec4(0.032014452, -0.020821465, 0.0826416, -0.002838458); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.06963679, -0.07560548, -0.069522075, 0.0038078027, -0.08002613, 0.13671301, 0.084461786, -0.039376218, 0.19136548, -0.123174496, 0.26566333, -0.16583005, -0.18664864, -0.023539122, -0.21928434, -0.026818147) * go_0(-1.0, -1.0); result += mat4(0.16660932, -0.18558703, 0.37230486, 0.118128106, -0.14098641, 0.14659132, -0.22217897, 0.12952235, -0.4139033, -0.04308319, 0.12885277, -0.17986743, -0.23556231, -0.08351981, -0.43240538, 0.019033253) * go_0(-1.0, 0.0); result += mat4(-0.18008037, -0.04448665, 0.011906908, -0.023056917, 0.18136618, -0.04723555, -0.0050158803, -0.14823224, -0.2105281, 0.023047728, -0.14040631, -0.03178526, -0.13477588, -0.01820428, 0.058358394, 0.23792502) * go_0(-1.0, 1.0); result += mat4(0.07363309, -0.061728477, 0.03573137, -0.0050971056, -0.012813505, -0.17236637, 0.1697835, 0.055788577, -0.22263195, 0.10324512, 0.58971673, -0.4872246, -0.1555681, 0.032747746, -0.096495196, 0.070196226) * go_0(0.0, -1.0); result += mat4(0.14174286, 0.099460006, -0.088765986, 0.58350676, -0.025177564, -0.46004987, 0.37007022, -0.11437029, -0.5164534, -0.60465246, 0.38859612, -0.32846406, 0.050266482, -0.20334712, 0.18316261, -0.19327633) * go_0(0.0, 0.0); result += mat4(-0.09377763, -0.0012762006, -0.028991895, -0.26523829, 0.20173682, 0.037923716, -0.03174243, 0.07103378, -0.10764164, -0.30752546, 0.20556998, -0.1892279, 0.08115748, -0.023550175, -0.07627362, 0.11746628) * go_0(0.0, 1.0); result += mat4(-0.06998859, -0.017997518, 0.069938794, -0.14943017, -0.14179112, 0.16643842, -0.110231474, 0.08895815, -0.24074875, 0.3277253, -0.07435203, -0.23452802, 0.039962552, -0.07145652, -0.022511544, -0.04571222) * go_0(1.0, -1.0); result += mat4(-0.059785757, -0.23771374, -0.030571314, 0.25222278, 0.106601834, 0.34398326, 0.14511436, -0.03867526, -0.38982397, -0.11944689, 0.12997924, -0.13079585, 0.005729482, 0.012653905, -0.063693404, 0.09632285) * go_0(1.0, 0.0); result += mat4(-0.04933823, 0.0547175, 0.050636575, -0.10060694, 0.1344485, 0.19752938, -0.100068115, -0.028829506, -0.14096203, -0.079092234, 0.092109434, 0.011606209, -0.04052607, -0.008347507, 0.06956573, -0.028109524) * go_0(1.0, 1.0); result += mat4(0.21918017, -0.11115073, 0.2262453, -0.06889667, -0.11256312, -0.07438075, -0.088454485, 0.13672407, -0.06905764, 0.08128395, 0.016103368, 0.050190717, 0.09691516, 0.05845721, 0.4886816, 0.041121427) * go_1(-1.0, -1.0); result += mat4(-0.3449472, 0.09711974, -0.13881907, -0.018265123, 0.27855873, -0.07030004, 0.29545054, 0.37216932, 0.08657718, 0.099066615, -0.10574013, -0.17667885, -0.14855732, -0.11351448, 0.66945946, 0.11312157) * go_1(-1.0, 0.0); result += mat4(0.2526151, -0.04594331, -0.06606611, 0.09104881, 0.06857995, -0.075284235, -0.17664689, 0.21578754, 0.0696524, 0.09142951, 0.080997564, -0.0682772, -0.0011445724, -0.11736295, 0.2519232, -0.101926275) * go_1(-1.0, 1.0); result += mat4(-0.12913518, 0.058357026, 0.195421, -0.15651494, 0.2877076, 0.0033844314, -0.07831594, 0.052855384, -0.031295884, 0.03301088, -0.18408822, 0.06732994, 0.23742151, -0.12568143, 0.22810535, -0.11545694) * go_1(0.0, -1.0); result += mat4(-0.49203303, -0.22656603, 0.1723193, -0.51250046, -0.09742038, 0.758559, -0.3387505, -0.6193586, 0.14136684, 0.27679884, -0.050113205, 0.31041816, -0.36475047, -0.48746544, 0.3233227, 0.4579754) * go_1(0.0, 0.0); result += mat4(0.46636763, 0.1507748, -0.2581362, 0.15413165, -0.17160143, 0.14256273, -0.074575804, -0.099299066, -0.0017214464, 0.13778336, -0.07378213, -0.15489665, -0.10533715, -0.0011083825, 0.39584312, 0.0023906573) * go_1(0.0, 1.0); result += mat4(0.026959421, -0.06391859, 0.0034752619, 0.14521928, -0.0010877338, -0.032619733, 0.005375293, -0.018952755, 0.03381545, -0.007652831, 0.034141563, 0.046016496, 0.11219674, 0.030913852, 0.077403754, 0.17192438) * go_1(1.0, -1.0); result += mat4(0.040326044, 0.17290725, -0.1220239, -0.09594783, -0.025229257, 0.17913155, -0.26623353, -0.033396784, -0.03075146, 0.009143897, -0.0136083895, -0.13886899, 0.075683735, -0.11584183, 0.22182357, 0.19350322) * go_1(1.0, 0.0); result += mat4(0.15726025, -0.10215694, -0.060057458, 0.26487043, -0.04075552, -0.016496127, 0.0015382086, 0.108562306, 0.026795091, 0.0441233, -0.08754318, -0.0460157, 0.048422016, 0.14107347, 0.07986661, 0.1047697) * go_1(1.0, 1.0); result += vec4(0.0766796, 0.08115133, -0.05703058, 0.14025708); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_2_tf //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.18038331, 0.21830973, -0.10019419, -0.022745568, -0.14944611, -0.15669158, 0.46361133, -0.07289843, 0.02976627, -0.09000817, 0.113060996, 0.05635241, 0.012762965, -0.022688959, 0.01629751, 0.061114635) * go_0(-1.0, -1.0); result += mat4(0.024338024, -0.10004009, -0.13709056, -0.0851965, 0.23927099, -0.024349794, -0.16574804, 0.084686354, -0.047885604, 0.09688507, -0.12733915, 0.06980246, 0.11480734, 0.014669346, -0.07505829, 0.04676309) * go_0(-1.0, 0.0); result += mat4(0.054203495, 0.011881634, -0.036115017, -0.0686298, -0.13682245, -0.15678032, 0.057050128, -0.03368558, 0.13011025, 0.033391044, -0.09841339, -0.027057761, -0.18701133, 0.20852546, -0.13660902, 0.0005817616) * go_0(-1.0, 1.0); result += mat4(-0.08077834, 0.35952288, -0.07647382, -0.0033230998, 0.13929126, -0.09155619, 0.14128102, 0.16005981, 0.18161216, -0.09485738, 0.0029118075, 0.052682754, 0.03242074, 0.08299826, 0.073796146, -0.06446532) * go_0(0.0, -1.0); result += mat4(-0.36655015, 0.4606936, 0.19073649, 0.31655258, -0.006838053, -0.579939, 0.089126326, -0.14021218, -0.3437716, 0.16714323, 0.17705944, -0.22418492, -0.3883696, -0.2302651, 0.2581861, 0.21983066) * go_0(0.0, 0.0); result += mat4(0.0992383, -0.014257871, -0.023896435, 0.19868234, 0.0408007, 0.07995299, 0.16102871, -0.11668251, 0.22458278, -0.05587917, 0.19373615, -0.016202094, -0.25106144, 0.15634494, 0.11624891, -0.2930768) * go_0(0.0, 1.0); result += mat4(0.024616942, 0.36248252, -0.14779098, -0.019894283, -0.007111256, 0.010641561, -0.09541178, 0.21236233, 0.009501827, 0.08132797, -0.13983901, 0.027207611, 0.038444366, -0.013995817, -0.16242191, 0.03294123) * go_0(1.0, -1.0); result += mat4(0.0131698875, -0.18124102, -0.13503514, -0.06099072, 0.07422735, -0.20906176, -0.049005672, 0.08739405, -0.031758767, -0.1978915, 0.23094437, 0.54512614, 0.21338555, -0.011205669, -0.23727885, -0.29533875) * go_0(1.0, 0.0); result += mat4(-0.0010255767, -0.07168225, -0.033568826, 0.22161655, -0.087293416, 0.11350447, 0.13653576, 0.061226424, -0.13074352, 0.058425818, 0.038460605, 0.2749964, -0.012814839, 0.085885845, -0.038151987, -0.17960808) * go_0(1.0, 1.0); result += mat4(0.19728905, -0.040724937, -0.18270236, 0.046735186, 0.03507326, 0.119867206, -0.12691991, 0.18119748, -0.052895024, 0.11348764, -0.043787055, 0.004703516, 0.006752757, -0.06939761, -0.009801806, -0.075640485) * go_1(-1.0, -1.0); result += mat4(0.051735226, 0.1732299, -0.10672899, 0.0320877, -0.4913656, 0.2102274, 0.43920282, 0.059108034, 0.08349019, -0.16517872, 0.15436842, -0.1075667, 0.022741623, -0.26693836, 0.3645307, 0.017874828) * go_1(-1.0, 0.0); result += mat4(0.034464058, 0.014929155, 0.054227423, 0.14167373, -0.0023630706, -0.08904212, 0.11918041, -0.034539603, 0.06048089, -0.06807333, 0.14447778, 0.035260547, 0.09979546, -0.1924939, 0.14596114, -0.12069667) * go_1(-1.0, 1.0); result += mat4(-0.04427228, -0.23673469, 0.010357103, -0.2907043, -0.06845721, -0.078984015, 0.06867713, -0.058163825, -0.12154615, 0.08430951, 0.1922373, 0.030108064, -0.43081748, -0.38715646, -0.022240646, -0.15403675) * go_1(0.0, -1.0); result += mat4(0.46885306, -0.33421394, -0.6695223, -0.41841158, 0.30317923, 0.24244753, -0.1047785, -0.18656285, 0.06261881, -0.4405616, 0.24233986, 0.40070608, 0.81440526, 0.11305212, -0.8826317, -0.023478031) * go_1(0.0, 0.0); result += mat4(-0.07879348, -0.024378026, -0.041883785, -0.17030984, 0.23229122, -0.011237109, 0.12058088, 0.20766267, -0.36519575, 0.09599417, -0.1271098, 0.06990154, 0.21161246, 0.041002538, -0.36046275, 0.007304667) * go_1(0.0, 1.0); result += mat4(0.10873893, 0.003872542, -0.13476561, -0.036068805, -0.054637462, 0.02304618, 0.04707738, -0.2856381, 0.07124422, 0.010866545, 0.20484549, -0.008342406, -0.43660247, -0.041055538, 0.33536008, -0.060022205) * go_1(1.0, -1.0); result += mat4(0.1966458, 0.0016302796, -0.25712642, -0.09639119, -0.006955351, 0.10882133, 0.1107341, 0.062697805, -0.1074494, 0.17361663, 0.6429869, -0.39846307, -0.26302996, 0.048710946, 0.40387508, 0.4299715) * go_1(1.0, 0.0); result += mat4(0.18948616, 0.24086732, -0.064474985, -0.11069709, 0.1279659, -0.13438123, -0.028438117, 0.125883, 0.018153818, -0.21942288, 0.020390838, -0.22797634, -0.10821287, -0.17175092, 0.122016855, 0.20699544) * go_1(1.0, 1.0); result += vec4(-0.05101961, -0.060740646, -0.024465766, 0.058471628); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_3_tf //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.14533128, 0.07266841, 0.13238011, -0.23328504, 0.031516243, 0.058471266, -0.06394412, 0.090752736, -0.0042359144, 0.12357294, -0.04377495, 0.0011743477, 0.05412243, -0.08146249, 0.04002749, -0.032876283) * go_0(-1.0, -1.0); result += mat4(-0.036972385, -0.15238069, -0.3453321, -0.36025128, 0.07597202, -0.02368151, -0.3889606, 0.34607083, 0.3133179, -0.21712309, -0.4210954, 0.21450534, 0.15226828, 0.25326282, 0.45327064, -0.3350824) * go_0(-1.0, 0.0); result += mat4(0.019018406, -0.33060563, -0.092601225, 0.14970545, 0.1441509, -0.19228427, -0.032771986, 0.26331595, 0.052981265, -0.06627376, -0.08634131, 0.038706224, 0.13403937, -4.4842476e-05, 0.049002815, -0.12719193) * go_0(-1.0, 1.0); result += mat4(0.17527401, -0.0035254909, -0.047959115, -0.4526988, -0.07510284, 0.0013256798, -0.07539148, 0.24220634, -0.08708839, -0.14494033, -0.17085724, -0.099797316, 0.0068515535, -0.08918779, 0.27164719, -0.1702649) * go_0(0.0, -1.0); result += mat4(0.31848368, 0.48983255, -0.44140294, -0.65174145, -0.004199057, 0.19494705, 0.5196497, -0.027118586, 0.032509074, -0.23900363, -0.14489244, 0.36314297, -0.23168536, -0.20960593, 0.61471456, 0.12401275) * go_0(0.0, 0.0); result += mat4(-0.24317405, 0.21560913, 0.15564032, 0.11606844, -0.15039803, -0.59578896, 0.14100945, -0.026194477, 0.37237462, -0.49472088, -0.15215331, -0.38820064, -0.25089455, -0.29643852, -0.09513793, 0.019779462) * go_0(0.0, 1.0); result += mat4(0.12498539, 0.0710632, -0.25012368, -0.2272255, -0.08647026, 0.12277892, 0.011025097, -0.12168395, -0.13489573, 0.016708186, -0.15583871, -0.057124946, 0.1216943, 0.019803725, 0.06952334, -0.032985855) * go_0(1.0, -1.0); result += mat4(0.28794885, 0.33783793, -0.14469545, -0.081780486, -0.50320613, -0.067601606, -0.06847453, -0.021648854, -0.34295765, 0.15071863, -0.06619896, -0.084465064, 0.31909832, 0.015414661, 0.14930317, -0.11295768) * go_0(1.0, 0.0); result += mat4(0.24530606, 0.25526014, 0.09971985, -0.07749641, -0.2361951, -0.07997673, 0.03617294, 0.02959561, -0.4498983, -0.014073485, -0.20587012, 0.06396779, 0.1262825, 0.027433183, 0.14469334, 0.011538011) * go_0(1.0, 1.0); result += mat4(-0.038572453, -0.023108613, -0.039481267, -0.012160024, -0.004521989, -0.028665857, 0.04295255, 0.10580258, 0.05439479, -0.072261885, 0.11030243, 0.08934696, 0.09133867, 0.017547369, 0.097613186, 0.05491059) * go_1(-1.0, -1.0); result += mat4(-0.09972817, 0.057730395, 0.12665828, 0.32861367, -0.16186063, 0.0745509, 0.2394045, -0.08687853, -0.034404907, -0.05843572, 0.0684561, -0.1355754, 0.19248672, -0.60372186, 0.12583947, 0.4388962) * go_1(-1.0, 0.0); result += mat4(0.10341107, 0.061113223, 0.08773817, -0.082504354, -0.16612078, 0.2681751, 0.019737698, -0.17122322, -0.135949, 0.3048101, 0.087803006, 0.11373851, 0.013192192, -0.27022064, 0.35529897, -0.15321451) * go_1(-1.0, 1.0); result += mat4(-0.032835662, 0.11123062, -0.11322452, -0.17300649, 0.04680824, 0.12849288, 0.17269878, -0.048671383, 0.05189037, -0.009078046, 0.22105052, 0.013008137, -0.009738674, 0.15391739, 0.20969556, 0.14189166) * go_1(0.0, -1.0); result += mat4(-0.47377753, 0.3038031, 0.18604809, 0.1931698, -0.2964668, -0.12287907, -0.7107761, 0.26619422, -0.33923018, 0.19200724, 0.013786281, -0.17496964, 0.079325035, -0.3694445, 0.0054486147, -0.33018264) * go_1(0.0, 0.0); result += mat4(0.14903802, -0.028043179, 1.5238678e-05, 0.021232028, 0.16025065, 0.14746875, -0.22831628, -0.12177345, 0.038778774, 0.32188168, -0.042017702, 0.27155936, 0.17920609, 0.04099755, 0.28527525, 0.074623376) * go_1(0.0, 1.0); result += mat4(0.057019282, -0.112741895, 0.030361209, 0.14567861, 0.056265317, -0.01573537, -0.06707608, 0.016657263, 0.09829025, -0.026795063, 0.023042196, 0.09438241, -0.025483066, -0.052787006, 0.19730279, 0.021218104) * go_1(1.0, -1.0); result += mat4(0.19868211, -0.01531125, 0.108596824, -0.035456363, 0.0033609823, 0.057961613, -0.013726211, 0.101742364, 0.33357215, 0.14468077, 0.29711527, -0.24662566, -0.119014986, -0.1899639, 0.11246697, -0.0035374009) * go_1(1.0, 0.0); result += mat4(-0.05602109, -0.15539522, 0.010730943, 0.057116497, -0.02037749, 0.084210664, -0.028235348, 0.10574697, 0.056925274, 0.07922333, -0.090088, 0.1615985, -0.0044301567, -0.089945644, 0.024176618, -0.041844133) * go_1(1.0, 1.0); result += vec4(0.0015292584, -0.043625206, -0.09429898, -0.06280405); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_4_tf //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.06051604, -0.028152643, -0.21418124, 0.13032125, 0.42565975, -0.09571944, -0.34494513, 0.30004, -0.073245734, -0.028659137, 0.0032105136, -0.05009555, -0.048971225, 0.04814533, 0.002843805, -0.046224426) * go_0(-1.0, -1.0); result += mat4(-0.07495975, 0.018714864, 0.21229684, -0.13614887, 0.79988647, -0.0697328, 0.38232988, 0.24165109, 0.25947478, -0.0009418982, -0.17369923, 0.10007766, 0.024117598, 0.028611807, 0.15090801, -0.06344829) * go_0(-1.0, 0.0); result += mat4(-0.07982219, 0.0900347, 0.007609254, -0.0034791247, 0.013611781, -0.13560618, 0.09685799, 0.06276075, 0.134693, -0.14370437, -0.25175703, -0.0016138123, -0.0075672898, -0.13325731, -0.061100446, 0.0059743375) * go_0(-1.0, 1.0); result += mat4(-0.039018434, -0.19668463, -0.43018532, 0.31886247, 0.4965479, 0.114569925, 0.19110382, 0.27343535, 0.0707728, -0.11877004, -0.25827697, 0.37012872, 0.1474777, 0.07056952, -0.14965728, 0.061595406) * go_0(0.0, -1.0); result += mat4(0.506543, -0.16268773, 0.455319, -0.0702646, 0.70102173, -0.14041683, 0.70184857, 0.4817842, -0.3389246, -0.14463086, 0.13763213, -1.1259074, 0.47722015, 0.38352612, -0.04293366, -0.5604627) * go_0(0.0, 0.0); result += mat4(0.17606944, 0.15897374, 0.13499324, 0.29241478, -0.032824475, 0.11128662, -0.22204424, -0.051803727, 0.013195331, -0.42040786, -0.3950585, 0.70745844, 0.38646924, -0.19080774, -0.15171832, -0.10742828) * go_0(0.0, 1.0); result += mat4(-0.039278325, 0.18421806, -0.044948544, 0.07902063, -0.2149251, 0.09913459, -0.09743655, -0.26899317, -0.002695496, -0.07554527, -0.22373366, 0.17830558, -0.047994815, -0.06789183, -0.06755918, -0.104452066) * go_0(1.0, -1.0); result += mat4(-0.0493473, -0.30411786, -0.056439694, -0.06582185, -0.21309847, 0.100670904, -0.22966193, -0.045954112, 0.12728062, -0.25081897, -0.094699375, -0.4036555, 0.060854495, -0.64373237, -0.21522263, -0.6683476) * go_0(1.0, 0.0); result += mat4(0.063481025, 0.11744312, -0.043330096, 0.33817932, -0.06679828, -0.23207302, -0.10188898, -0.10590511, 0.058780864, 0.047292337, -0.11834696, 0.10076128, -0.036641665, 0.30200714, -0.0002892557, -0.10303763) * go_0(1.0, 1.0); result += mat4(-0.10842604, 0.042055763, 0.29702973, -0.07409644, -0.030164458, -0.012098744, -0.06396587, -0.08787527, 0.051854923, 0.12997511, 0.11468497, 0.15022379, 0.007814715, 0.014517445, 0.025484756, 0.01078619) * go_1(-1.0, -1.0); result += mat4(-0.29229385, 0.040265664, -0.15376821, 0.075579196, -0.05593569, -0.045405343, 0.12099204, 0.1571252, 0.17841713, 0.04673325, 0.14550509, 0.08603346, -0.049786013, 0.06121843, -0.16273825, -0.13857752) * go_1(-1.0, 0.0); result += mat4(0.06903744, 0.2628764, -0.13582836, -0.35678583, -0.13821034, -0.019381443, -0.19570538, -0.09298511, 0.08965436, 0.09745909, 0.20055099, 0.024967568, 0.08144204, 0.004633625, 0.12809834, -0.009431525) * go_1(-1.0, 1.0); result += mat4(0.09784006, 0.010729353, 0.046643205, -0.110926524, -0.21556224, 0.00016300633, 0.122175336, 0.15004392, 0.013864355, 0.24767809, 0.13865592, 0.0155424485, -0.1450483, -0.15688781, -0.06195043, -0.13745981) * go_1(0.0, -1.0); result += mat4(0.018991318, 0.55401963, 0.11709872, -0.028442185, -0.46035343, -0.10215539, -0.60193926, 0.47882316, -0.23346989, 0.037200127, 0.22814943, -0.08231696, -0.36430013, -0.011152757, 0.48752213, 0.29796222) * go_1(0.0, 0.0); result += mat4(-0.07258066, -0.023222538, 0.23230423, -0.30317304, 0.03942911, -0.06899803, 0.23778579, 0.07418621, -0.17443737, 0.33387753, 0.007354842, -0.123447575, -0.1745315, 0.11071779, -0.11949625, -0.22832453) * go_1(0.0, 1.0); result += mat4(-0.024909232, -0.0308135, 0.12170621, -0.13298757, 0.045828197, -0.1532345, -0.06633672, 0.23591088, 0.04964077, 0.14091493, 0.038343724, -0.029780807, 0.05762822, -0.048930667, -0.02434709, 0.07109019) * go_1(1.0, -1.0); result += mat4(-0.16039175, 0.3004474, -0.17278233, 0.13677922, 0.18838613, 0.15054552, 0.32901475, -0.1288333, 0.26378244, -0.05119892, 0.34533516, 0.25180495, 0.19452183, 0.0843233, -0.08029368, 0.39877903) * go_1(1.0, 0.0); result += mat4(-0.07097129, -0.26492423, -0.055032317, -0.093516104, -0.11795062, 0.04086253, -0.07989471, 0.059686553, 0.09378249, 0.45851848, 0.2510942, 0.19599153, 0.019765077, -0.02920918, -0.04125142, -0.13859107) * go_1(1.0, 1.0); result += vec4(0.04400571, -0.04015565, 0.0140529545, 0.05474095); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_5_tf //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.014236042, -0.0031431736, -0.1551387, 0.12515116, -0.28528872, 0.36161992, 0.15750743, -0.17111474, 0.13792591, -0.0657419, -0.17471549, 0.14650472, 0.034169197, -0.019157575, 0.23520657, -0.20358163) * go_0(-1.0, -1.0); result += mat4(0.02015035, 0.12993371, 0.11199667, -0.09854378, 0.5001741, 0.03462961, 0.24919736, 0.08505297, -0.20902094, -0.24141377, -0.15360375, 0.049974803, -0.037157424, -0.048510186, 0.20106035, -0.118480384) * go_0(-1.0, 0.0); result += mat4(0.086798504, -0.009607818, 0.034812123, -0.005187592, 0.0351509, 0.021755, -0.04996161, -0.041231696, 0.0020545553, 0.015730752, -0.07507172, 0.018597523, -0.02393343, 0.07624775, 0.03892451, -0.0025574185) * go_0(-1.0, 1.0); result += mat4(0.035725456, 0.06809103, 0.51926994, -0.39983147, -0.16402833, -0.1243394, -0.25922915, 0.28285915, 0.15959994, -0.2351732, 0.2650535, -0.30193794, -0.11468332, 0.050777763, -0.51894253, 0.4408367) * go_0(0.0, -1.0); result += mat4(-0.27042082, 0.22243942, 0.14902467, 0.38428563, 0.46612173, 0.5169912, -0.22330502, -0.11300288, -0.36141354, 0.0668681, 0.2984152, 0.1275798, -0.24121419, 0.2952039, -0.45109174, -0.3822957) * go_0(0.0, 0.0); result += mat4(0.26543504, -0.05742226, -0.052103903, -0.013124308, -0.14358385, -0.04024543, 0.07665455, -0.012301872, -0.18752757, -0.03913891, 0.038205814, -0.006583095, -0.25550908, -0.25725332, -0.12454206, -0.0058936924) * go_0(0.0, 1.0); result += mat4(-0.0018946569, 0.019746022, -0.13080788, 0.11450627, -0.013743845, -0.027179785, -0.14425103, 0.07109661, 0.023703793, 0.086905524, 0.03151253, 0.0132474145, 0.041018624, 0.04548913, 0.2718715, -0.20008296) * go_0(1.0, -1.0); result += mat4(-0.076830454, 0.11652955, 0.5068201, -0.3082819, 0.058615055, -0.006765798, -0.057522714, 0.049981344, -0.006897243, -0.21763432, 0.16896053, -0.21176189, -0.061227098, 0.03566485, 0.08901554, -0.050980624) * go_0(1.0, 0.0); result += mat4(0.02327798, 0.07662976, 0.034811985, -0.03238033, -0.0021881019, -0.030997375, -0.069672935, 0.04040273, -0.1217442, 0.104173124, 0.09862539, 0.020557549, -0.022286594, 0.10287763, -0.021694934, 0.07542515) * go_0(1.0, 1.0); result += mat4(0.124069154, -0.08579466, -0.07816314, 0.11332851, -0.034682628, -0.11038275, 0.04750615, -0.096100725, 0.039588403, -0.15149672, -0.05529172, 0.034304325, -0.022520235, -0.05023852, -0.2674731, 0.21886522) * go_1(-1.0, -1.0); result += mat4(-0.1948599, -0.14946899, -0.39548838, 0.18042913, -0.007919619, 0.19826505, 0.23789087, 0.009140256, 0.11857748, 0.18215668, 0.13606293, -0.09209675, -0.080678545, -0.020431137, -0.07728839, -0.051353537) * go_1(-1.0, 0.0); result += mat4(-0.07616472, -0.0032800382, -0.045657665, -0.039144326, -0.37786487, -0.08877774, 0.053579114, -0.070886396, 0.011311804, 0.107276045, 0.013236154, 0.009832061, 0.08292063, 0.12258811, 0.0005569043, -0.009806432) * go_1(-1.0, 1.0); result += mat4(-0.28062925, 0.15946878, -0.1021801, -0.06471589, -0.26999477, 0.21230288, -0.14243907, 0.2555922, -0.09608517, 0.26339412, 0.20891234, -0.23538485, 0.33958244, -0.12569186, 0.43289876, -0.33462036) * go_1(0.0, -1.0); result += mat4(0.16265294, 0.2625464, -0.34452894, 0.2233622, 0.13850005, -0.42999864, -0.5385177, -0.11035979, 0.51662, -0.78238726, -0.09422375, 0.83759475, 0.44468537, 0.14301361, 0.108906105, 1.1596143) * go_1(0.0, 0.0); result += mat4(-0.73757625, -0.12369605, 0.23523071, 0.006587637, -0.15445381, 0.22757277, 0.052819528, 0.10183905, -0.07912228, -0.16998893, -0.13360223, 0.014348178, -0.17778571, -0.41047302, 0.10241381, -0.08526306) * go_1(0.0, 1.0); result += mat4(0.14712952, 0.048995696, 0.05299946, -0.06817572, 0.1498064, -0.079825334, 0.40354064, -0.31789717, -0.1998377, 0.00955295, -0.32318407, 0.30898204, -0.039571725, -0.026203401, -0.16292085, 0.08574385) * go_1(1.0, -1.0); result += mat4(-0.6353329, -0.56000775, -0.17279743, 0.18198174, -0.19555812, 0.056538377, 0.34365895, -0.07799055, 0.19011354, -0.13952748, 0.029196098, -0.19596763, -0.069196045, -0.17402656, 0.07948411, -0.016226962) * go_1(1.0, 0.0); result += mat4(0.25592864, 0.083498634, -0.28515807, 0.10789751, 0.0043962947, 0.07085363, 0.048724182, -0.025131436, -0.0049440865, -0.033094388, -0.032935806, 0.04266025, 0.20026933, 0.0927841, -0.006839351, -0.013012285) * go_1(1.0, 1.0); result += vec4(0.02021373, 0.0014037411, 0.0012718709, 0.017278494); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x1x1x56 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_1_tf //!BIND conv2d_2_tf //!BIND conv2d_3_tf //!BIND conv2d_4_tf //!BIND conv2d_5_tf //!BIND conv2d_6_tf //!SAVE conv2d_last_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_2 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_5 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_6 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_8 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_9 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_10 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_12 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_13 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) vec4 hook() { vec4 result = mat4(-0.0067711817, 0.08160003, 0.0247279, 0.03084815, -0.026977416, -0.02120602, -0.025078611, -0.029852165, -0.011627478, -0.012742972, 0.022736797, -0.0028815821, -0.007515677, 0.0172887, -0.023259213, 0.009608947) * g_0; result += mat4(-0.028660107, -0.014015208, -0.027838672, -0.013171922, 0.0029435428, 0.027047642, -0.017478354, 0.022834882, -0.037572853, -0.0034044068, -0.0149029335, -0.013362301, 0.009827443, -0.015742151, -0.0074795415, -0.0022266617) * g_1; result += mat4(-0.07579662, -0.039754186, -0.066026606, -0.046816852, 0.1099032, 0.043956704, 0.073109835, 0.04680284, -0.06896613, -0.008838632, -0.044584926, -0.01319039, -0.0021152915, -0.04503326, 0.027061926, -0.028334105) * g_2; result += mat4(0.15458213, 0.059769996, 0.09327123, -0.028782733, 0.023459995, -0.15390377, -0.13432898, -0.1127775, 0.072764635, -0.0020463336, 0.034736466, -0.0012086042, -0.05847183, -0.029952323, 0.052969377, 0.09590908) * g_3; result += mat4(-0.07476772, -0.016574614, 0.04131183, 0.017335678, 0.009654406, 0.072183535, -0.002266456, 0.086873695, 9.310129e-05, 0.0056416965, -0.004188391, 0.023132093, -0.05183336, -0.025825873, -0.03684392, -0.0075729224) * g_4; result += mat4(0.00878842, 0.03869637, -0.035759524, 0.003345386, -0.064184256, -0.034568302, -0.06672922, -0.0686381, -0.06794392, -0.10685906, 0.04679947, -0.012535639, 0.006932529, -0.007783515, 0.109123886, 0.13804391) * g_5; result += mat4(-0.03160699, 0.050473, -0.09030729, 0.0649397, 0.11466501, 0.17912874, -0.0081851315, 0.052244574, 0.051632743, 0.061941486, 0.06546816, 0.12174249, -0.05104755, -0.018193979, -0.032196652, -0.035292786) * g_6; result += mat4(0.013612735, -0.0024100312, -0.068611205, -0.07369285, -0.019647537, -0.066944756, -0.010012875, -0.06785739, -0.062246565, -0.087313406, -0.044278186, -0.09368995, 0.052555013, 0.13604961, 0.05645059, 0.08763303) * g_7; result += mat4(0.04218486, -0.05028401, 0.059086576, -0.03545452, 0.027737848, 0.0043074046, 0.0011001764, -0.073026665, -0.04094988, 0.044061556, -0.009812515, 0.06841999, -0.06612581, 0.037223976, -0.07759491, -0.04356598) * g_8; result += mat4(-0.027558247, 0.014248466, -0.019813016, -0.058107473, -0.016717663, -0.020424338, 0.0053625097, -0.009917319, 0.013678771, 0.0113340765, 0.0061787106, -0.036083996, -0.020179711, -0.011310535, 0.054827053, -0.0008278952) * g_9; result += mat4(0.028690035, -0.012079616, 0.11931408, -0.048533775, 0.069336995, 0.0049852817, 0.013774468, 0.035233382, -0.07384821, 0.0003354423, -0.0059171803, -0.04503906, 0.08727279, 0.005138857, -0.17724465, 0.055782065) * g_10; result += mat4(-0.20744391, 0.24348328, -0.3145766, 0.17026486, -0.022870807, -0.01648648, -0.05912279, -0.012555373, -0.066004686, 0.03182394, 0.16285324, -0.1221846, -0.31816196, 0.007928748, 0.43180224, -0.015949022) * g_11; result += mat4(0.16363169, 0.14781676, -0.2377973, -0.1571377, -0.09038187, 0.0046504294, 0.033955004, -0.051421452, 0.046735536, 0.006827522, -0.121338, 0.12671822, 0.15833299, -0.1858712, -0.1942371, 0.17336044) * g_12; result += mat4(-0.018145572, -0.015550516, 0.044410378, 0.046016492, 0.084021375, 0.05327457, -0.008270992, -0.045435544, 0.07185879, -0.131923, 0.26721445, -0.26745328, -0.07093472, 0.042701527, 0.13793674, -0.095621444) * g_13; result += vec4(0.016836504, 0.010161949, 0.021351453, 0.01278978); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Depth-to-Space //!HOOK MAIN //!BIND MAIN //!BIND conv2d_last_tf //!SAVE MAIN //!WIDTH conv2d_last_tf.w 2 * //!HEIGHT conv2d_last_tf.h 2 * //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * vec4 hook() { vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size); ivec2 i0 = ivec2(f0 * vec2(2.0)); float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x]; float c1 = c0; float c2 = c1; float c3 = c2; return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Upscale_CNN_x2_S.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(-0.0057322932, 0.12928207, -0.056848746, 0.18680117, -0.0306273, 0.25602463, 0.053723164, 0.20419341, 0.0018709862, 0.022848232, -0.04105527, 0.10169034, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(0.009471417, -0.12957802, 0.096014425, 0.21836184, 0.00021601951, -0.22997683, 0.23666254, 0.41192335, 0.021762101, 0.0047863554, 0.008233427, 0.108514786, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(-0.01156376, -0.18988979, 0.04614705, -0.044767227, 0.01050636, -0.26426336, 0.23741047, 0.0027636609, -0.027718676, -0.14202335, -0.016650287, -0.06637125, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(0.057809234, -0.11033858, 0.056533534, -0.06292466, 0.13880666, -0.18710336, 0.2441031, -0.25326246, 0.0032683122, -0.026437074, 0.0023248852, 7.640766e-05, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(-0.49110603, 0.4429004, -0.44015464, -0.41174838, -0.87738293, 0.7808468, -1.0929365, -0.59699076, -0.18409836, 0.185138, -0.11773224, -0.17097276, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(0.10580959, -0.055947904, -0.03431237, -0.080236495, 0.14862584, -0.15393938, -0.18872876, -0.3170681, 0.03559387, -0.003990826, 0.021298569, 0.012844483, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(-0.040715586, -0.25781113, 0.08896714, -0.1225879, -0.15790503, -0.54010904, 0.29588607, 0.10401059, 0.003413123, -0.108357325, 0.0112870345, -0.11888622, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(0.0049315444, 0.02376202, -0.08224771, 0.121118225, -0.041512914, -0.027994309, -0.585988, -0.069672115, -0.017247835, 0.0056576864, 0.04319012, 0.055003505, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(0.37521392, 0.15916082, 0.059708964, 0.19046007, 0.8120325, 0.38343868, 0.3436578, 0.5287958, 0.16570656, 0.06957687, 0.014022592, 0.074799836, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(-0.01050964, -0.00939481, 0.17684458, 0.027366742); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.011029496, 0.05866063, -0.09460646, -0.017664742, -0.022488879, 0.18384217, -0.00397663, -0.064733066, 0.08466802, 0.10667488, 8.0212536e-05, 0.0908869, 0.13580276, 0.00097438256, 0.12176522, -0.08218466) * go_0(-1.0, -1.0); result += mat4(0.16062798, -0.10190268, 0.03280682, 0.05621916, -0.009684231, -0.08464307, 0.17058301, -0.096469186, 0.1967505, -0.1450099, 0.093607284, -0.28240147, -0.21377413, 0.10079291, -0.1741522, 0.17330575) * go_0(-1.0, 0.0); result += mat4(-0.060160473, 0.06316997, 0.0046929033, -0.049405966, 0.13851729, 0.06830702, -0.0586872, -0.040827133, 0.007052838, -0.03576886, -0.111261636, 0.039155316, -0.07380389, -0.09369825, 0.04471156, 0.09678487) * go_0(-1.0, 1.0); result += mat4(-0.36683616, -0.035950605, -0.24414362, -0.009159744, 0.19335322, -0.099253505, 0.075083904, -0.00076695543, 0.65291303, -0.25599423, 0.19827642, 0.065899536, -0.07423247, -0.068967685, 0.0050554527, -0.060272824) * go_0(0.0, -1.0); result += mat4(-0.020688485, -0.83178276, 0.11104878, 0.26454413, 0.13655476, 0.37675047, -0.22219229, -0.01751935, 0.44552696, 0.92510307, 0.16063261, -0.62011045, 0.19366647, -0.06996067, -0.2504841, 0.00803723) * go_0(0.0, 0.0); result += mat4(0.0051537007, -0.057168536, -0.16110587, 0.25232598, -0.04447099, 0.11997351, 0.14808103, -0.34443566, -0.26212573, -0.21970181, 0.2724405, 0.21050811, -0.07949061, -0.064808235, -0.21208277, -0.0042361654) * go_0(0.0, 1.0); result += mat4(-0.0888952, -0.20169449, 0.19144905, -0.016882861, -0.013283103, 0.07552998, -0.24686803, 0.012453213, -0.065454446, -0.016123284, -0.47316182, 0.070926026, 0.09219782, 0.13118166, 0.074736096, 0.0077910526) * go_0(1.0, -1.0); result += mat4(0.5832154, 0.1138069, -0.039765622, 0.3182784, -0.25497997, 0.0013993139, 0.39285088, -0.48511526, -0.39891505, -0.19094779, -0.082146175, -0.20826934, 0.020590555, -0.0012490178, -0.4398621, 0.14377014) * go_0(1.0, 0.0); result += mat4(0.21917395, 3.4314657e-05, 0.25734863, -0.3433305, 0.015720673, 0.2676127, -0.06807297, 0.15040149, -0.23638041, -0.0050233034, -0.13666134, 0.4542111, -0.033572577, -0.08450588, -0.23341487, 0.053490847) * go_0(1.0, 1.0); result += mat4(-0.17482175, 0.057647135, 0.33135444, 0.0850751, -0.1718849, -0.0854123, 0.036795795, -0.13874969, -0.10903869, -0.19007301, -0.06064334, -0.03786032, -0.036696054, 0.07844446, 0.012523185, -0.01562906) * go_1(-1.0, -1.0); result += mat4(-0.04411997, -0.10331819, 0.10050193, 0.12406485, 0.07431592, 0.30109692, -0.17511666, -0.13263564, -0.10192587, 0.07821255, -0.22415096, 0.25552443, 0.17881326, -0.13914281, 0.109979235, -0.0016463579) * go_1(-1.0, 0.0); result += mat4(-0.01911644, -0.15412527, 0.028903123, 0.20831817, 0.00375175, 0.08110953, 0.074919395, -0.17581624, -0.015677985, 0.06504228, 0.08817818, -0.12518327, -0.09537373, 0.028905088, -0.051288474, 0.054334078) * go_1(-1.0, 1.0); result += mat4(0.2852779, -0.28924024, 0.36805123, 0.21079305, -0.28336474, 0.1679663, -0.08641141, -0.10699407, -0.16090055, 0.1287612, -0.15910125, 0.05734755, 0.15883245, 0.0053026294, 0.080674745, 0.0505137) * go_1(0.0, -1.0); result += mat4(0.17639062, 0.3790122, -0.19588692, -0.020314282, 0.26197383, 0.09014768, 0.19696823, -0.41025418, -0.08308115, -0.33279485, -0.22528782, 0.06172439, -0.1365661, -0.13094363, -0.005086559, 0.089024484) * go_1(0.0, 0.0); result += mat4(0.05262993, 0.0006296959, 0.1657725, -0.32591924, 0.12126701, 0.061543245, -0.10526848, 0.041583937, 0.094976954, 0.09416157, -0.22019257, -0.058390073, -0.2073888, 0.057273377, 0.19558284, 0.004208022) * go_1(0.0, 1.0); result += mat4(0.30005738, 0.18478931, -0.23342943, 0.22455733, -0.016488122, 0.099634305, 0.31620836, -0.15731157, 0.09595808, 0.0013774688, 0.48273298, -0.07027936, -0.18764344, -0.26194447, -0.11794225, -0.012173601) * go_1(1.0, -1.0); result += mat4(0.117986746, -0.13846518, -0.019614812, -0.3011192, 0.5501164, 0.3408611, -0.40090847, 0.15706886, 0.13050972, 0.051776595, 0.20792943, 0.23389706, -0.22965533, -0.053367328, 0.3911586, -0.032988597) * go_1(1.0, 0.0); result += mat4(0.054753624, -0.008485731, -0.2451672, 0.17528129, 0.13657846, 0.010480436, 0.07651423, -0.43316832, 0.12736236, 0.13804524, 0.12529011, -0.30946237, -0.14423579, 0.08403089, 0.24335162, 0.057288036) * go_1(1.0, 1.0); result += vec4(0.012077211, 0.013045883, 0.0380778, -0.02908858); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.036115196, -0.06971895, -0.07508942, 0.016036168, 0.12120111, 0.24536026, 0.044755507, -0.20663576, 0.029635755, -0.15427187, 0.027148994, -0.20795093, 0.10170582, 0.077919215, 0.66063017, -0.4632968) * go_0(-1.0, -1.0); result += mat4(-0.0052889925, -0.019060908, -0.08660142, -0.022095207, -0.08097976, -0.015142803, -0.18552722, -0.078493506, -0.16293915, -0.20099808, -0.08370822, 0.3701389, 0.09094984, 0.2487225, 0.24338846, 0.044003833) * go_0(-1.0, 0.0); result += mat4(-0.061406493, -0.017232792, -0.10917424, 0.11203319, 0.040699825, -0.019294346, 0.084953666, -0.018133596, 0.07209552, 0.016069936, 0.17805555, -0.089537814, 0.15809004, 0.1027023, 0.15044671, -0.15530108) * go_0(-1.0, 1.0); result += mat4(0.0948676, -0.040305693, -0.005591629, -0.048048403, -0.07547777, 0.056606572, 0.021390207, 0.32600567, -0.20805131, -0.099587254, 0.029613169, 0.0092129605, -0.29429698, -0.09898621, 0.44470885, -0.89487344) * go_0(0.0, -1.0); result += mat4(-0.122259885, 0.11445877, 0.06666907, 0.1869428, -0.1553992, -0.1658741, 0.2988138, -0.57746625, -0.34609964, 0.11169158, -0.41877756, 0.38075635, 0.21293911, 0.09640372, -0.12754214, -0.08026104) * go_0(0.0, 0.0); result += mat4(0.15128808, 0.050087795, 0.09219755, -0.18080945, 0.0044571217, -0.046019405, -0.1289922, 0.20305426, 0.19601224, 0.04667917, 0.17465587, 0.027672665, 0.18441725, 0.06845396, 0.11288585, -0.23283863) * go_0(0.0, 1.0); result += mat4(-0.072962, -0.06639447, 0.049347494, -0.1386401, 0.10396071, 0.08187777, -0.04280746, 0.07390891, 0.06628344, 0.037797406, 0.021885803, -0.013147403, 0.22376558, 0.36243078, 0.12874891, -0.0023783944) * go_0(1.0, -1.0); result += mat4(0.074945286, 0.16045591, -0.11798349, 0.12910712, 0.054760084, -0.095626175, -0.047832094, 0.03493912, 0.11817307, 0.037452437, -0.14301221, -0.027356789, -0.052390423, 0.11373512, 0.07686775, 0.010008694) * go_0(1.0, 0.0); result += mat4(-0.023999173, -0.091900624, 0.02388157, 0.03173873, 0.0065633506, -0.033716757, -0.1198324, 0.12057766, 0.026465805, -0.07517131, -0.07760598, 0.060463097, 0.07345541, 0.046037503, 0.21101558, -0.26785463) * go_0(1.0, 1.0); result += mat4(0.15544604, -0.03902825, 0.04630384, -0.25173616, -0.0691359, 0.07476507, 0.009071253, 0.089964196, -0.26539803, -0.3958477, -0.22155671, 0.20735882, -0.105860494, -0.003996804, -0.044815883, 0.39544627) * go_1(-1.0, -1.0); result += mat4(0.6169709, 0.23717614, -0.37884676, -0.7484867, 0.020169826, -0.30718836, 1.0965588, -0.20711036, -0.39149985, -0.06843563, -0.06522909, 0.103805855, 0.03265825, -0.15137726, 0.12837899, -0.01294922) * go_1(-1.0, 0.0); result += mat4(-0.23638196, -0.4560866, -0.11948684, -0.1464144, 0.10690008, 0.007835961, 0.11864342, -0.13101323, -0.16509797, 0.075027354, 0.08122998, 0.13451207, 0.0011890623, 0.052157886, 0.08372405, -0.07085038) * go_1(-1.0, 1.0); result += mat4(-0.21997726, -0.16488647, -0.0291317, 0.17997476, 0.1493211, 0.027494298, 0.0034613227, -0.3207727, 0.18699001, 0.14728633, -0.042895135, -0.07612043, 0.125076, -0.14714554, -0.03480009, -0.22753975) * go_1(0.0, -1.0); result += mat4(-0.5342686, -0.7426105, -0.38294584, 0.42549992, 0.46053204, 0.7867879, 0.106234804, -0.041163098, 0.5198579, -0.5219404, 0.14809476, -0.41802374, 0.06810794, -0.15122683, -0.047409, 0.13178343) * go_1(0.0, 0.0); result += mat4(-0.50428164, 0.18220626, 0.35510704, -0.081787474, 0.03155813, 0.019284263, 0.0032388573, -0.20513348, -0.05385551, 0.17803182, -0.26206362, 0.2870375, 0.008557827, 0.08401449, -0.027598893, -0.010791235) * go_1(0.0, 1.0); result += mat4(0.16657415, 0.067647465, 0.093076974, -0.14438486, -0.10017002, 0.0022367141, 0.03250936, -0.052794546, -0.009178676, -0.019673595, -0.0016697067, -0.15424626, -0.112123474, -0.11079971, 0.011987111, -0.11747758) * go_1(1.0, -1.0); result += mat4(-0.023021797, -0.058703423, -0.037978355, -0.062433913, -0.13130441, 0.048656322, 0.056839373, 0.109036915, -0.07823158, 0.14785293, 0.058555078, -0.11679035, -0.14002073, 0.07395252, 0.098268874, -0.06710464) * go_1(1.0, 0.0); result += mat4(0.14906375, 0.030001195, -0.10338215, 0.0662968, -0.161953, -0.13682815, 0.09563142, 0.009514228, -0.009491218, 0.06737101, -0.1393389, 0.15231515, -0.073147796, 0.00767062, 0.028675212, 0.014213088) * go_1(1.0, 1.0); result += vec4(0.018736731, -0.0026039074, 0.050130025, -0.055364225); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_2_tf //!SAVE conv2d_last_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.019100675, -0.014241565, 0.004667036, -0.03865062, 0.106731094, 0.026099661, 0.014594411, -0.011881356, 0.0040967264, -0.004626336, 0.006469508, 0.010875305, -0.033909045, -0.085905954, 0.07861378, 0.019452631) * go_0(-1.0, -1.0); result += mat4(0.20777655, -0.060354974, 0.0023840065, -0.064121604, -0.17397617, 0.019293457, -0.09707183, 0.080641985, 0.01025124, -0.017382381, 0.008661793, -0.010995665, 0.21943407, -0.115574986, 0.14471593, -0.068836235) * go_0(-1.0, 0.0); result += mat4(0.057942886, -0.06311754, 0.2253396, -0.04159292, -0.020731755, 0.007877151, 0.041525815, 0.025278691, 0.03041967, -0.025137542, 0.024364179, -0.024543528, 0.029438615, -0.015506873, 0.081686, -0.07812221) * go_0(-1.0, 1.0); result += mat4(0.054237515, 0.0676094, -0.0047708177, 0.0043467237, -0.10032304, -0.020498628, 0.04240586, 0.07272254, 0.0784221, 0.017945962, -0.022310399, -0.013134622, 0.015638694, -0.10001543, 0.1043031, 0.05898838) * go_0(0.0, -1.0); result += mat4(-0.021652509, 0.35796642, 0.059497777, 0.23948468, 0.15454951, -0.10017235, -0.19072174, -0.44812536, -0.03974552, 0.04529369, 0.22207436, 0.026222564, -0.09705454, 0.5623026, -0.3354105, -0.017278556) * go_0(0.0, 0.0); result += mat4(-0.053682446, -0.03411237, -0.09399936, 0.15128824, -0.07463, -0.042020727, 0.0031783928, 0.13481957, -0.07731454, 0.044114403, -0.23085599, 0.060444202, -0.15015422, 0.0018040676, -0.18684982, 0.2812511) * go_0(0.0, 1.0); result += mat4(0.0029329916, 0.001596018, 0.0007512241, 0.016544111, -0.04876942, -0.05272409, 0.037884697, 0.049948208, 0.015518177, 0.11368592, -0.03815777, -0.013149978, -0.027638039, 0.107719295, -0.04115787, 0.02745414) * go_0(1.0, -1.0); result += mat4(0.016691081, 0.010204119, 0.04078854, 0.01613337, 0.03325829, 0.0114824055, -0.017286912, -0.07284126, -0.110984206, -0.21041764, 0.0089543555, 0.18986733, 0.01537506, -0.2059135, 0.029074017, 0.013117443) * go_0(1.0, 0.0); result += mat4(0.013965926, 0.029871881, 0.0034499036, -0.011343668, 0.022120327, -0.0068748263, 0.009324342, -0.039081004, 0.08032371, 0.050809264, 0.035050742, -0.2032847, 0.06305391, -0.021958945, 0.038569167, -0.22465245) * go_0(1.0, 1.0); result += mat4(0.046307724, -0.012419472, 0.007673863, -0.042344846, 0.011042414, 0.016994251, -0.018166406, -0.016955731, -0.13240299, 0.01768431, -0.027607648, 0.0699927, -0.02840628, 0.004414203, 0.0049618417, 0.011084679) * go_1(-1.0, -1.0); result += mat4(-0.119954154, -0.007455482, -0.031108133, -0.009946449, 0.0077065965, 0.01660345, 0.032943666, 0.016376585, 0.10273124, 0.1556573, -0.24643841, 0.107307844, -0.068235755, 0.0561896, -0.0104672015, 0.042693343) * go_1(-1.0, 0.0); result += mat4(-0.01634601, 0.04195375, -0.10401894, 0.047641944, -0.034602515, -0.0034419263, -0.010457858, 0.015194475, -0.03962551, -0.030031368, 0.16036317, 0.019283568, -0.05877721, 0.016504882, -0.15523468, 0.018161612) * go_1(-1.0, 1.0); result += mat4(-0.08083991, 0.0024665035, -0.049373373, 0.030371357, 0.0113322195, -0.014676956, 0.011646689, -0.01142667, 0.124930486, 0.06625774, -0.045840867, -0.009693036, -0.012649251, -0.07388084, 0.008790075, 0.0013844534) * go_1(0.0, -1.0); result += mat4(-0.33941835, -0.2763476, -0.118311435, -0.063535266, 0.20936015, 0.13731301, 0.13443594, 0.07464433, 0.059650812, -0.36973104, 0.16444235, -0.37082872, 0.06432777, -0.18283032, -0.044489607, -0.13895285) * go_1(0.0, 0.0); result += mat4(0.13533665, 0.08268915, -0.03675727, -0.14348659, 0.0186255, -0.05051692, 0.056702953, 0.0061717895, 0.047663026, -0.088188455, 0.23254345, -0.014015464, 0.08400204, -0.0073777726, 0.2202068, -0.12366078) * go_1(0.0, 1.0); result += mat4(0.04361004, 0.046543695, 0.0064863074, -0.03358146, -0.022602187, 0.018138997, -0.011071864, 0.010244091, -0.019814799, -0.17250171, 0.040823266, -0.040131986, 0.010125854, 0.020660749, 0.0020435036, -0.010819304) * go_1(1.0, -1.0); result += mat4(-0.004810193, -0.11286074, 0.051985834, 0.04788631, -0.023950428, 0.036145125, -0.038203828, 0.052401308, 0.022986965, 0.26420745, -0.06076917, -0.09252999, 0.03164547, 0.15652153, -0.037934, -0.0035418556) * go_1(1.0, 0.0); result += mat4(0.03358366, -0.005219482, 0.007060882, -0.06569114, -0.02941682, 0.00966056, -0.0153679885, 0.019905418, -0.107232265, -0.03405676, -0.044340115, 0.26892832, -0.04723829, -0.02589829, 0.004563232, 0.19318114) * go_1(1.0, 1.0); result += vec4(-0.00346731, -0.0046263863, -0.004627155, -0.0057769152); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Depth-to-Space //!HOOK MAIN //!BIND MAIN //!BIND conv2d_last_tf //!SAVE MAIN //!WIDTH conv2d_last_tf.w 2 * //!HEIGHT conv2d_last_tf.h 2 * //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * vec4 hook() { vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size); ivec2 i0 = ivec2(f0 * vec2(2.0)); float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x]; float c1 = c0; float c2 = c1; float c3 = c2; return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Upscale_CNN_x2_VL.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(0.3053028, -0.037464816, 0.113983095, 0.12537485, -0.18630321, 0.084269725, -0.01351514, -0.20190673, -0.12298384, -0.037622184, -0.070214555, -0.19367279, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(-0.41849324, 0.099702746, -0.04276645, -0.047299717, 0.20074473, 0.14217933, 0.15571699, 0.19553481, 0.21868695, -0.053848714, 0.016413521, 0.14117444, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(0.030540446, -0.052293833, 0.0715466, -0.31160545, 0.07808315, -0.16860045, 0.032828577, -0.2955024, -0.110374965, 0.04043687, -0.014024628, 0.058699366, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(-0.10727635, 0.054200135, 0.20853694, 0.21086875, 0.122690216, -0.091823794, 0.310609, -0.01738923, -0.0013488946, 0.10835534, -0.077265196, 0.086751856, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(-0.77150255, 0.40530515, -0.41257596, -0.14367618, 0.46888494, 0.2650122, -0.934199, 0.40476102, 0.32293493, 0.20251967, 0.19891106, -0.29698747, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(-0.12505147, -0.41904053, -0.065798186, 0.34075752, 0.026240354, -0.2977496, 0.032647505, -0.003566783, 0.10290523, -0.23417123, -0.06014203, 0.094735645, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(0.11207838, -0.04062474, 0.023897955, 0.08605987, -0.020888371, 0.045541205, -0.07231824, -0.25884083, -0.11796847, -0.002691391, 0.0050435597, 0.02756291, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(0.4615728, 0.041790638, 0.08971143, 0.20213957, -0.38537467, 0.19938901, 0.08594364, -0.08621994, -0.08163473, -0.133266, -0.09561729, -0.014209637, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(0.0787417, -0.0483673, 0.07621572, -0.060169693, -0.013465177, -0.17152289, 0.02515561, 0.17675288, -0.05173998, 0.10768042, -0.029858522, -0.013957215, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(0.0072128535, -0.05658625, 0.052939568, -0.1760861); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf1 //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { vec4 result = mat4(-0.112743355, 0.0422517, 0.21350034, -0.0967133, 0.16265953, 0.0022497, 0.015078242, 0.08204187, 0.035236806, -0.0468228, -0.09464228, -0.001864949, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0); result += mat4(0.25631642, -0.41485596, -0.16662048, 0.13201024, 0.057921384, 0.2240005, -0.30038536, -0.08305622, 0.2228756, 0.32263795, 0.10608189, -0.18616734, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0); result += mat4(0.08997524, 0.11516871, 0.19212262, -0.035154644, 0.11612274, -0.04056247, 0.14974374, 0.029173585, -0.07629641, -0.14353512, 0.041081246, 0.20230265, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0); result += mat4(0.2262286, 0.055954933, -0.14499907, 0.17314723, 0.16590612, -0.06688698, -0.11118816, -0.012938116, -0.043101817, 0.026133137, 0.2958395, 0.06543993, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0); result += mat4(-0.07311521, -0.3041244, -0.47978505, -0.6350967, -0.17432262, 0.34965977, 0.25399777, -0.16590433, -0.49957857, 0.0549526, -0.40869385, -0.08780993, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0); result += mat4(-0.3014447, -0.00021343959, -0.14953177, 0.028001398, -0.14931908, -0.14910097, -0.13287953, -0.45026535, 0.17378895, 0.024704922, -0.027308129, -0.10292025, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0); result += mat4(-0.06732655, -0.13119644, 0.066014715, 0.081011154, -0.15154321, 0.2407805, 0.07733481, 0.12312706, 0.1741804, 0.008495716, -0.14125362, -0.043644864, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0); result += mat4(0.11465958, 0.42001364, 0.011069392, 0.3203028, -0.058801666, -0.37830314, -0.030540617, 0.2245139, -0.11310525, -0.14845212, 0.19957744, 0.25789997, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0); result += mat4(-0.16037206, 0.21326372, 0.020099448, 0.018666709, 0.122083254, -0.16033986, -0.10725163, 0.2556128, 0.1650688, -0.10475823, 0.048623525, -0.103755645, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0); result += vec4(0.007717166, -0.027800834, 0.0795002, 0.0053199283); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.0056740534, -0.21186607, -0.18014967, 0.118979976, -0.0015611284, -0.07708486, 0.060131397, 0.11653345, 0.027150517, 0.10837246, 0.08583816, -0.14032431, 0.017552888, 0.0035846964, 0.03980114, 0.064649396) * go_0(-1.0, -1.0); result += mat4(-0.03289318, -0.12004539, 0.26514888, -0.15079662, 0.04214227, -0.027273783, -0.027950313, 0.19614808, 0.18510003, -0.10346252, -0.029836183, 0.09174428, -0.0088710375, -0.18273513, 0.06601674, 0.009983851) * go_0(-1.0, 0.0); result += mat4(0.08476211, 0.043996535, 0.056711517, 0.009976895, 0.07039107, -0.024862664, -0.059921104, 0.046850603, 0.04983447, 0.04863198, 0.21777405, -0.0576961, 0.045321796, -0.0060038245, 0.096396215, -0.10842004) * go_0(-1.0, 1.0); result += mat4(-0.15746164, 0.041757874, 0.035169285, -0.1734288, -0.24219254, -0.13318908, 0.2272079, -0.02902605, 0.07750601, -0.1467191, -0.12296749, -0.07533314, -0.07073083, 0.17909113, 0.04789308, 0.17245363) * go_0(0.0, -1.0); result += mat4(0.057547905, 0.1464685, -0.33115456, -0.26956198, -0.26298407, -0.059824817, 0.022509675, -0.09251868, 0.36277944, -0.2072429, 0.21095088, -0.45492023, 0.07428653, 0.1593302, -0.2945834, 0.12825087) * go_0(0.0, 0.0); result += mat4(-0.1318458, 0.27804148, 0.037600737, 0.12047866, 0.0065036337, 0.0017241207, 0.060497303, -0.14786585, -0.15149063, 0.02731698, 0.048886403, -0.0025970868, -0.026979815, 0.07348884, 0.015636757, -0.107966796) * go_0(0.0, 1.0); result += mat4(-0.079988025, -0.01626299, 0.06517438, 0.086406484, -0.1484504, 0.070595, 0.20620634, 0.09713373, -0.13620836, 0.012067949, -0.00068703433, -0.038030174, 0.22300471, -0.0012400965, -0.014827909, -0.08927486) * go_0(1.0, -1.0); result += mat4(0.15634936, 0.052028038, 0.038081627, 0.12720168, 0.07342066, -0.04318368, -0.0065998454, 0.12109317, -0.45398173, 0.03666754, -0.17773737, 0.038516667, -0.13009632, -0.007457001, -0.013938809, 0.09776142) * go_0(1.0, 0.0); result += mat4(0.029636936, 0.12864171, 0.11347291, -0.11812842, -0.0870342, 0.035678383, 0.050338242, 0.045754932, -0.07072752, 0.010447726, 0.039642975, -0.08795004, -0.1191525, 0.00967509, 0.13485421, -0.053204738) * go_0(1.0, 1.0); result += mat4(-0.011072695, -0.09613245, -0.09094804, 0.028029291, -0.04031162, 0.15690295, 0.25094184, -0.21776834, 0.06524669, 0.06412185, -0.052852992, -0.08097702, -0.039127756, 0.036357917, 0.104585476, 0.25095442) * go_1(-1.0, -1.0); result += mat4(-0.08328618, -0.006246033, 0.099708706, -0.014916097, 0.17727195, 0.4369228, 0.14760216, 0.06707674, 0.025167737, -0.022487842, -0.038962565, 0.15380669, 0.08125089, 0.09844594, 0.33538374, -0.003161368) * go_1(-1.0, 0.0); result += mat4(-0.0128195705, -0.05475118, -0.037705053, -0.0012077648, -0.17425515, 0.091487505, -0.12909423, 0.0074876705, 0.13438368, 5.778033e-05, 0.04563314, -0.12185897, -0.053612474, -0.049824294, -0.12851205, 0.12856449) * go_1(-1.0, 1.0); result += mat4(-0.025741795, 0.01867236, -0.00027440622, 0.10502768, 0.27042285, -0.14947751, 0.11143123, 0.2575913, -0.07414089, -0.33919522, -0.13194235, -0.20088726, 0.23121537, -0.08197353, 0.06693911, 0.015411386) * go_1(0.0, -1.0); result += mat4(0.09143717, 0.22842278, 0.06501074, -0.20009698, -0.042117566, -0.23452093, -0.074082755, -0.10612558, 0.077631965, 0.08343657, -0.07657599, -0.43297377, 0.7092466, -0.16272525, 0.17222248, -0.056038965) * go_1(0.0, 0.0); result += mat4(0.081200436, 0.046752565, 0.028254949, 0.18820632, 0.096592255, 0.05896745, 0.14845169, 0.034777895, 0.07195204, -0.1908046, -0.015341971, 0.02606145, -0.010377239, 0.0755547, -0.15285216, 0.047916733) * go_1(0.0, 1.0); result += mat4(-0.06825636, -0.049540907, -0.024328846, 0.03506251, 0.2060094, 0.054119263, -0.06671269, 0.052428722, 0.055792283, -0.14336903, -0.03180757, 0.013760968, -0.037398104, -0.06880077, -0.023608573, 0.0360965) * go_1(1.0, -1.0); result += mat4(-0.16937497, -0.30156836, 0.0021435453, 0.025772978, -0.17990975, 0.046133514, -0.32447076, -0.083382785, -0.081322014, -0.022132374, -0.05319431, 0.11794733, 0.08943906, 0.12927428, 0.105764806, -0.051034793) * go_1(1.0, 0.0); result += mat4(-0.011012306, 0.047636557, 0.050260928, 0.051847618, 0.010985655, -0.13752967, 0.023869954, 0.07011459, -0.18244945, 0.07239806, -0.013638856, -0.026982805, 0.11395993, -0.031304818, -0.08714153, 0.077115685) * go_1(1.0, 1.0); result += mat4(0.08707592, 0.2265186, 0.13363098, -0.039588258, -0.029561255, 0.019238092, 0.024606103, -0.0019022018, -0.062285982, -0.0629511, -0.03753033, 0.109805316, 0.016018672, -0.08284564, -0.04092752, -0.030386891) * go_2(-1.0, -1.0); result += mat4(0.0016500859, 0.01616536, -0.099148355, 0.24161765, 0.028064307, -0.028680569, 0.054400917, -0.1978921, -0.08584302, -0.096797146, -0.06546965, -0.09342837, 0.030265866, 0.07057579, -0.02080932, 0.053178705) * go_2(-1.0, 0.0); result += mat4(-0.030304352, 0.047440585, -0.04248429, 0.08568772, -0.051317703, 0.036739342, 0.00865767, -0.018183297, -0.07335176, 0.025001721, -0.068509035, 0.1814819, -0.09756565, -0.024179723, -0.05959287, 0.0352454) * go_2(-1.0, 1.0); result += mat4(0.023015196, -0.022870664, -0.12028372, -0.111095205, 0.11065281, -0.19900022, -0.24012049, -0.017028643, -0.13484617, 0.050107025, 0.10741765, 0.037951697, 0.013090438, -0.0010045726, -0.029447839, -0.1859787) * go_2(0.0, -1.0); result += mat4(0.17922719, -0.24138594, -0.44595388, -0.032014426, 0.06897096, 0.07125395, 0.1944457, -0.035794795, -0.24022278, -0.13230884, -0.1277025, 0.21229011, -0.12249393, 0.06141907, 0.2687936, -0.26896995) * go_2(0.0, 0.0); result += mat4(0.0397242, -0.30710965, 0.28815824, -0.06642567, -0.07588877, -0.019552408, 0.0057806037, 0.11465521, 0.03560534, -0.10640553, 0.023589289, -0.16667193, 0.02066607, -0.01026633, -0.02655378, 0.082493655) * go_2(0.0, 1.0); result += mat4(-0.007902949, -0.08501038, -0.029395591, -0.07072227, -0.01800967, -0.14564751, -0.08372804, -0.049974415, 0.1756957, -0.02042449, -0.04413007, -0.016873527, -0.2385717, -0.001741017, 0.08298281, -0.019873247) * go_2(1.0, -1.0); result += mat4(-0.01803727, 0.0642893, 0.21513617, 0.066888265, -0.042107955, -0.123470366, 0.045296013, -0.11958806, 0.48208967, -0.027188249, 0.12136116, 0.05246265, 0.13522038, -0.016297493, 0.028486907, -0.059840377) * go_2(1.0, 0.0); result += mat4(-0.1373251, -0.11281026, -0.06418318, 0.08444032, 0.062874556, -0.009133875, -0.049571835, -0.042995855, 0.12483249, -0.025967957, -0.11202483, 0.09862257, 0.099986054, 0.009230306, -0.09042664, 0.046612263) * go_2(1.0, 1.0); result += mat4(0.03203309, 0.106030256, 0.045741174, -0.020529225, -0.028610658, -0.055219248, -0.21404657, 0.07746393, -0.059359375, 0.0033258004, -0.0054513607, 0.06856653, 0.18043655, -0.119936846, -0.05639265, -0.10240379) * go_3(-1.0, -1.0); result += mat4(-0.0004331875, 0.10426754, -0.008130048, 0.012795991, -0.14372933, -0.40797862, 0.105197415, -0.0041354536, -0.079792455, 0.0914027, 0.012418237, -0.11449173, 0.020261409, -0.14681602, -0.13355242, 0.18290488) * go_3(-1.0, 0.0); result += mat4(0.052306626, 0.010864275, -0.072627716, -0.009773121, 0.09484167, -0.09631301, 0.14896165, -0.21220942, -0.11994051, -0.002957136, -0.118194886, 0.08661347, 0.10005298, -0.029620873, 0.101668894, 0.0242806) * go_3(-1.0, 1.0); result += mat4(-0.055188183, -0.06322889, 0.12994595, 0.03140751, -0.092755616, 0.04239107, 0.18460171, 0.08471877, 0.014203371, 0.13608724, 0.035351243, -0.07883493, -0.10067456, 0.14417742, 0.0054235114, 0.100745104) * go_3(0.0, -1.0); result += mat4(-0.043811034, -0.16055201, -0.11927185, 0.20517266, 0.16734722, 0.27720267, 0.1205665, 0.045803893, -0.07874647, 0.06764307, -0.11157022, 0.080770165, -0.044105835, -0.03276538, -0.10945451, 0.100562036) * go_3(0.0, 0.0); result += mat4(-0.044731796, -0.12854387, -0.061937924, -0.21604767, -0.036132332, -0.024353411, -0.16718283, 0.14903957, -0.11620588, 0.14563644, 0.23363836, 0.08400659, 0.15248756, -0.1424437, 0.112882614, -0.04096889) * go_3(0.0, 1.0); result += mat4(-0.0486021, -0.05714939, 0.042517707, -0.06106919, -0.12970918, -0.071898215, -0.044727243, -0.026308542, 0.05687118, -0.0394057, -0.109454155, -0.0021216893, 0.018588595, 0.08061093, 0.0500373, -0.0034918839) * go_3(1.0, -1.0); result += mat4(0.11269324, -0.17924047, -0.12965205, -0.07287767, -0.015830642, -0.044497102, 0.20014328, -0.14054494, 0.1232692, 0.2395109, 0.14093149, 0.03518561, -0.14088139, -0.09045081, -0.07283352, 0.053434785) * go_3(1.0, 0.0); result += mat4(0.020512339, 0.026349569, -0.06666101, 0.05554806, -0.03044066, 0.26656216, 0.019155584, -0.12118906, 0.087923005, -0.1716557, 0.050843164, 0.037432503, -0.030232614, 0.030457936, 0.04232163, -0.066400655) * go_3(1.0, 1.0); result += vec4(-0.0216415, 0.09015036, -0.030761974, -0.26541537); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf1 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.04688368, 0.13853125, 0.1714716, -0.03034447, -0.08090605, 0.1225867, 0.17535992, 0.012508419, -0.0010665918, -0.07481546, -0.15541986, 0.0671128, -0.029307734, -0.076674186, 0.03925896, -0.07140553) * go_0(-1.0, -1.0); result += mat4(-0.13273083, 0.062933214, 0.04200143, -0.0080243945, -0.120439716, -0.090192355, -0.022639645, 0.00020024918, -0.11211478, -0.12949537, 0.025783822, 0.009155746, 0.01004339, -0.0661901, 0.10630156, 0.053137038) * go_0(-1.0, 0.0); result += mat4(0.07113487, -0.16011865, -0.10838903, -0.0034704183, 0.110606894, -0.14915739, 0.036511585, -0.003103608, -0.0551775, -0.13140677, 0.05270299, 0.12139221, 0.02226174, 0.008415268, -0.06647426, 0.118130066) * go_0(-1.0, 1.0); result += mat4(-0.045172617, -0.0020388453, -0.27287582, 0.002428232, -0.2833772, 0.13788106, 0.073339015, 0.10666715, 0.08455194, 0.16499293, 0.089058325, 0.008815447, 0.034657538, -0.109856166, -0.11499077, -0.02918854) * go_0(0.0, -1.0); result += mat4(0.07910854, -0.26334837, -0.3246593, -0.08246522, 0.09211476, 0.40793833, -0.09658794, -0.14430091, -0.50632644, 0.087234974, 0.26298127, 0.3687086, 0.06492316, 0.23082961, 0.18233871, -0.09283792) * go_0(0.0, 0.0); result += mat4(-0.022744032, 0.21690565, 0.2694824, -0.12230013, -0.07969618, 0.21595429, -0.034979805, 0.008938489, 0.21289209, -0.446482, -0.042927746, -0.13587558, -0.032581557, -0.07182814, -0.054092336, -0.009542036) * go_0(0.0, 1.0); result += mat4(-0.0034912943, -0.080354184, -0.08577375, -0.1521193, 0.09809233, 0.034529503, -0.100664355, 0.008191219, -0.014303411, -0.02862216, -0.18669915, -0.12384598, 0.046499267, 0.093707144, 0.10661308, 0.15079576) * go_0(1.0, -1.0); result += mat4(-0.031025652, -0.0384342, 0.14258307, 0.25531343, 0.0075049917, -0.03966595, 0.062381975, 0.19593526, -0.2868182, 0.03162008, -0.4391041, -0.524017, -0.034463473, -0.0066741486, -0.24586639, 0.10521736) * go_0(1.0, 0.0); result += mat4(-0.07452321, -0.0227877, -0.025402244, 0.115727395, -0.039511252, -0.07785703, -0.013689458, 0.0066024344, -0.052957747, 0.011206241, -0.0021671024, 0.077190824, -0.11709912, 0.046635598, 0.123751156, -0.03712064) * go_0(1.0, 1.0); result += mat4(0.055411004, -0.0020031065, 0.06685547, -0.018829947, -0.06378933, -0.18389674, -0.0023551763, 0.0670314, 0.13038594, 0.0601923, -0.03035789, -0.019537423, -0.014483204, -0.056800704, 0.08663347, -0.106859975) * go_1(-1.0, -1.0); result += mat4(-0.06603686, 0.07360526, -0.0072026253, -0.06778907, -0.039178446, 0.012397263, -0.13482279, 0.05745685, -0.055182382, -0.10545766, 0.003857615, 0.041947857, -0.15239377, 0.041826613, 0.058879383, -0.0042669442) * go_1(-1.0, 0.0); result += mat4(-0.0697229, -0.010702144, -0.032265816, 0.013317131, 0.105028264, 0.21032134, 0.06845646, -0.018358687, 0.064568676, 0.08437135, -0.000723181, 0.1324007, 0.05527932, -0.049871888, -0.10125047, -0.005040889) * go_1(-1.0, 1.0); result += mat4(-0.006467578, -0.05120533, -0.011780779, -0.011742203, -0.34242442, -0.020819988, 0.17381702, -0.059836414, -0.028882682, 0.23210457, 0.16579404, -0.03708216, -0.23541835, -0.03290251, 0.029319672, 0.26189178) * go_1(0.0, -1.0); result += mat4(-0.30955994, -0.06408282, -0.16872866, 0.10767772, -0.041430887, 0.051697977, 0.12523535, -0.060389146, 0.026289431, 0.06359533, 0.13526368, 0.2479901, -0.3263977, 0.10216362, -0.0030894123, 0.046437826) * go_1(0.0, 0.0); result += mat4(0.10061438, -0.17047118, -0.21593021, -0.023389054, -0.17507865, -0.30822313, -0.22044766, 0.16078933, 0.07099252, -0.11573018, 0.24712858, -0.0659458, -0.037504572, -0.12297423, 0.03342632, -0.058119852) * go_1(0.0, 1.0); result += mat4(-0.020957774, -0.0224927, 0.04069268, -0.07911167, 0.074009344, 0.065916434, 0.008222278, 0.11625076, -0.25299504, 0.03357169, -0.021988, 0.015821831, -0.0021187372, -0.030700417, -0.004374924, 0.027358979) * go_1(1.0, -1.0); result += mat4(0.06549052, -0.048067164, 0.05489091, -0.28851983, 0.13378961, 0.026875904, -0.09877994, -0.19947459, -0.1274035, -0.022928834, -0.26344195, -0.025870804, 0.022505255, 0.0070861108, 0.121051334, -0.025964163) * go_1(1.0, 0.0); result += mat4(0.059426542, -0.0327433, 0.2313695, -0.07046268, 0.20479666, 0.027021704, 0.2564928, -0.11689885, -0.07407976, -0.019611249, 0.093463086, -0.121553615, 0.035009407, -0.008135333, -0.075931996, 0.047803063) * go_1(1.0, 1.0); result += mat4(-0.059434246, -0.1652242, -0.124611154, 0.04743711, 0.10530296, -0.13869187, -0.036534663, -0.035206333, 0.06067593, 0.06126907, 0.120151915, -0.06722673, 0.008103894, 0.037225723, -0.007520425, 0.065720856) * go_2(-1.0, -1.0); result += mat4(-3.6759695e-05, -0.036789574, 0.013370567, -0.037871476, -0.013454664, 0.15086569, 0.10164699, 0.057703357, -0.12871023, 0.12827681, -0.055057358, -0.040753044, -0.0142621, 0.08563361, -0.04615499, -0.03130452) * go_2(-1.0, 0.0); result += mat4(-0.117965914, 0.09056485, 0.07272314, 0.009695964, -0.11331058, 0.07467256, -0.08291521, 0.00937355, -0.04097737, 0.07752905, -0.017335521, -0.12539999, 0.039462104, -0.0007037007, 0.06034812, -0.09497377) * go_2(-1.0, 1.0); result += mat4(0.20828065, 0.0400099, 0.047638226, -0.046423353, -0.026133502, 0.098207295, 0.056742374, 0.017029466, -0.058164768, -0.046973787, -0.17328712, -0.0012984811, 0.050085854, 0.11296557, 0.12639083, 0.058543045) * go_2(0.0, -1.0); result += mat4(-0.098907426, 0.22031747, 0.101559944, 0.06616554, 0.026110496, 0.56487054, 0.23754556, -0.07540935, 0.31768414, -0.47653618, 0.015073956, -0.33731326, 0.087285936, -0.24593173, -0.26141426, 0.15003823) * go_2(0.0, 0.0); result += mat4(0.046026446, -0.13767281, 0.064847544, 0.07717139, 0.08544123, -0.11092969, 0.072325274, 0.010849038, -0.3055905, 0.66436774, 0.1434729, 0.0494463, 0.07115603, 0.083811216, 0.020431712, 0.06537088) * go_2(0.0, 1.0); result += mat4(-0.15532711, 0.030139687, 0.040853374, 0.11089222, -0.08150315, -0.015851755, -0.06787692, 0.096075505, -0.011956207, -0.0017758606, 0.1277494, 0.16156575, -0.038588695, -0.0626418, -0.041797023, -0.19467135) * go_2(1.0, -1.0); result += mat4(0.12917455, 0.017410474, -0.20125067, -0.08040003, -0.13494664, 0.17789102, -0.19909395, 0.08441434, 0.078570575, -0.06330619, 0.23767303, 0.5442659, -0.009227878, -0.021818208, 0.14318731, -0.09042824) * go_2(1.0, 0.0); result += mat4(0.097801, 0.09345441, 0.17846581, -0.14773296, 0.06536365, 0.07642184, -0.011880635, 0.02086135, 0.013336972, -0.053295113, -0.13410404, 0.027241753, 0.087728985, -0.044033397, -0.13098569, 0.009423933) * go_2(1.0, 1.0); result += mat4(-0.02488427, 0.0134966355, -0.0075000813, 0.07272353, 0.015842725, 0.13765687, 0.028079558, -0.08384948, -0.06666623, -0.023220664, 0.025091043, -0.055167805, -0.18826278, 0.04423603, 0.13499942, 0.059128854) * go_3(-1.0, -1.0); result += mat4(0.01935146, -0.030980906, -0.031569187, -0.0036869382, 0.036753897, 0.118464164, 0.15871695, -0.09842428, 0.023324292, 0.071796335, -0.07869346, -0.10751301, -0.2588698, 0.064011686, 0.17386378, -0.039197855) * go_3(-1.0, 0.0); result += mat4(0.08590827, 0.005497696, -0.026512025, 0.015661815, 0.1102415, -0.08268483, -0.0032903247, 0.10049029, -0.008157236, -0.035823178, -0.017570151, -0.081716835, -0.3531045, 0.010005245, 0.017141227, -0.016376914) * go_3(-1.0, 1.0); result += mat4(-0.16617337, -0.007689783, 0.00954665, 0.07117733, -0.001669262, -0.012331606, 0.051613946, 0.062780835, 0.06123557, -0.20243123, -0.19181818, 0.032895602, 0.19760677, 0.004464939, 0.12754539, -0.27360034) * go_3(0.0, -1.0); result += mat4(0.15006685, -0.083587274, -0.03215495, -0.16992462, -0.011944293, 0.058361508, -0.088097006, 0.023880545, -0.04168166, -0.06960282, -0.092672385, -0.057278465, 0.23540072, -0.1721208, -0.018213503, -0.23494521) * go_3(0.0, 0.0); result += mat4(-0.124885194, 0.1905868, 0.11108704, 0.03163991, 0.11383064, 0.101223364, 0.069428995, -0.14298953, -0.07609092, 0.13704266, -0.07749446, -0.0005389336, -0.04617235, 0.18011934, 0.08350316, 0.09416366) * go_3(0.0, 1.0); result += mat4(0.073356606, 0.067966126, -0.21285574, 0.0782625, -0.0034364646, -0.032581426, -0.05538558, -0.1317288, 0.14552782, -0.1132393, 0.13063973, -0.00833602, 0.0026844777, 0.028135289, -0.02536825, -0.028372496) * go_3(1.0, -1.0); result += mat4(-0.318728, 0.07862527, -0.12176221, 0.35010242, -0.029198067, 0.016302662, 0.17667587, 0.12605923, 0.1556697, -0.06061443, 0.05843511, 0.10891248, 0.01267106, -0.018492714, -0.15945031, -0.050723754) * go_3(1.0, 0.0); result += mat4(-0.21555941, -0.016813517, -0.084676236, -0.07545412, -0.14518794, -0.014592766, -0.2446481, 0.0530632, 0.0847341, 0.12342537, -0.028644923, 0.083479315, -0.04179012, 0.0025225023, 0.16006976, -0.026940256) * go_3(1.0, 1.0); result += vec4(-0.060742114, -0.037577342, 0.055704296, 0.03134311); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.13129333, -0.022117995, -0.009753253, 0.020439912, 0.044090994, -0.0916335, 0.0036765633, -0.11719207, -0.06413809, 0.04079378, -0.00085516454, -0.06306388, -0.12660664, -0.054126263, -0.005513979, 0.06364538) * go_0(-1.0, -1.0); result += mat4(-0.028422508, 0.23270117, -0.28674677, -0.10820166, 0.024321957, -0.0811145, -0.07290707, -0.02125165, -0.064260505, 0.052076746, -0.009654081, 0.08363882, -0.02037171, 0.15006389, 0.121593125, -0.011237004) * go_0(-1.0, 0.0); result += mat4(-0.14672333, 0.015381624, 0.1028172, -0.041823238, 0.0072677187, -0.042953942, 0.06426537, -0.0938381, -0.05990813, -0.04599802, -0.11264726, -0.027826328, -0.058160868, 0.10747306, -0.07327458, 0.07998872) * go_0(-1.0, 1.0); result += mat4(-0.08702181, -0.03750975, -0.045659006, 0.04488332, 0.09102003, 0.066556975, -0.04353586, 0.08994567, -0.13561495, -0.10653702, 0.006989605, 0.028230097, 0.07177144, 0.2938447, -0.00943923, 0.022120917) * go_0(0.0, -1.0); result += mat4(-0.1801194, -0.11119162, 0.1977298, -0.247902, -0.16654298, -0.07423158, 0.114130594, 0.0014401592, 0.006954727, -0.09810646, -0.051310766, 0.19487657, 0.2545855, -0.06328558, -0.04617056, 0.09444692) * go_0(0.0, 0.0); result += mat4(0.011378825, 0.16044368, 0.017211074, 0.14472178, 0.032992378, -0.008925819, 0.035120245, -0.012409223, 0.074333005, 0.1178002, -0.128956, -0.13624239, -0.2791275, 0.21457297, -0.1476131, 0.04874687) * go_0(0.0, 1.0); result += mat4(-0.03491764, -0.061763793, 0.05779039, 0.0054837577, -0.023937583, 0.08281698, 0.032306053, -0.014566218, 0.12738499, -0.0132100545, -0.051833414, 0.0057818824, 0.012158851, -0.20231532, -0.0043795826, 0.10285843) * go_0(1.0, -1.0); result += mat4(-0.22269921, -0.15135509, -0.039143335, 0.033390045, 0.06770212, -0.14538582, -0.08011057, 0.03796648, -0.025913516, 0.13925864, 0.18309896, 0.012709204, -0.24912506, 0.3217706, 0.0394195, 0.017977878) * go_0(1.0, 0.0); result += mat4(0.00080196525, 0.059145816, 0.05720508, 0.0056548906, 0.005168018, 0.09938438, 0.0200503, -0.05516137, 0.061309986, -0.019621318, -0.1541441, 0.019540716, 0.030571707, -0.09054893, 0.032851614, -0.27210873) * go_0(1.0, 1.0); result += mat4(0.27061436, -0.114008114, -0.0020118617, -0.1656827, 0.09770587, 0.029897455, -0.03307522, -0.04661818, 0.033011347, 0.18498488, -0.05162084, 0.087471776, -0.24665618, -0.12538423, -0.08123797, -0.010210389) * go_1(-1.0, -1.0); result += mat4(0.075188264, 0.0020608555, 0.18558815, 0.041179713, 0.11232638, 0.05507779, -0.19599183, 0.027942855, 0.06199144, 0.22141005, -0.06121163, 0.014993597, 0.24105869, -0.019737717, -0.112485714, 0.0157406) * go_1(-1.0, 0.0); result += mat4(0.09425698, 0.0207658, 0.12074599, 0.009430481, 0.11889248, -0.025782838, 0.0034711843, 0.05113582, 0.012531833, -0.0018606635, -0.09137569, 0.018120576, 0.4051155, 0.02222076, -0.16001017, 0.10981527) * go_1(-1.0, 1.0); result += mat4(-0.03582557, 0.014994796, -6.4688604e-05, 0.24618183, -0.11697727, 0.24388117, 0.038502026, -0.3511993, 0.101741396, -0.10748137, 0.035059888, -0.017535849, 0.09450039, 0.06541661, 0.12149035, 0.28798738) * go_1(0.0, -1.0); result += mat4(-0.27143848, 0.017990451, -0.69144464, 0.037944376, -0.04551905, 0.09263134, 0.4259611, -0.14107811, -0.10641847, 0.23065196, 0.040813655, -0.07789163, 0.3087666, 0.08190437, 0.16409059, -0.06455426) * go_1(0.0, 0.0); result += mat4(-0.08290655, -0.35286915, -0.18082355, -0.32229406, 0.1608227, 0.030915622, 0.09207708, 0.02655054, 0.039464593, 0.026095424, 0.052584656, 0.033881903, -0.01751319, -0.0011676399, 0.04002607, 0.1630013) * go_1(0.0, 1.0); result += mat4(-0.012021132, 0.12163766, -0.07410629, -0.06879096, 0.017859738, -0.039261997, -0.028677614, -0.23610398, -0.15963873, -0.0006119958, 0.11275506, 0.0082659265, 0.05677582, 0.08676638, -0.08669759, -0.10475464) * go_1(1.0, -1.0); result += mat4(0.12792721, 0.06888765, 0.31803077, 0.26002547, -0.067599155, -0.011822328, -0.2589909, -0.30024147, 0.11076704, 0.15200609, -0.018180368, -0.19146141, 0.22298847, 0.059484895, 0.034478076, 0.15610938) * go_1(1.0, 0.0); result += mat4(0.0870121, -0.016420847, -0.011579898, 0.097182855, -0.120095566, -0.06843338, -0.043460473, -0.060684606, -0.027540063, -0.008499213, 0.033570655, -0.06866259, 0.01429712, -0.07424434, 0.0009466247, 0.09142678) * go_1(1.0, 1.0); result += mat4(-0.03781424, 0.04587032, 0.03744051, 0.02712279, -0.051038064, 0.0669144, -0.02640278, 0.12384894, -0.0022533627, -0.010022036, 0.07536463, -0.030489929, 0.09418577, 0.155089, -0.011290433, -0.02102941) * go_2(-1.0, -1.0); result += mat4(-0.0053278613, -0.07160643, 0.039028414, 0.04123311, -0.10693177, -0.1170874, 0.07230816, -0.033255517, -0.119176835, 0.0786526, -0.11880206, -0.11354601, -0.037539184, 0.14404313, 0.069760695, 0.024738638) * go_2(-1.0, 0.0); result += mat4(0.03413808, -0.006487654, 0.10006853, 0.22228058, -0.13796462, -0.14042488, 0.04017443, -0.031790894, -0.06673143, 0.009888688, 0.08831443, -0.0045771743, -0.028375361, -0.04704813, 0.07128581, -0.07012518) * go_2(-1.0, 1.0); result += mat4(-0.06954315, -0.23728988, -0.14192343, -0.08236467, -0.2552115, 0.04102959, -0.06355397, -0.08340241, 0.17617856, 0.20281969, -0.16249381, 0.10843737, -0.04392261, -0.08587206, 0.053069845, -0.15482199) * go_2(0.0, -1.0); result += mat4(0.124981806, 0.12828638, -0.061472785, -0.20108232, -0.14905351, -0.40766275, -0.35427195, -0.13183996, 0.09307428, -0.07697028, 0.06702549, -0.22656697, 0.019868268, -0.19361132, 0.08784669, 0.20249842) * go_2(0.0, 0.0); result += mat4(-0.004661343, -0.09333453, -0.24876262, -0.07906779, 0.110697776, -0.37069768, -0.042212646, -0.0046135853, -0.2254257, -0.023392014, 0.031476703, -0.045574382, -0.12675518, -0.076056994, -0.08228006, -0.040303517) * go_2(0.0, 1.0); result += mat4(0.16182694, 0.0512523, 0.051189836, 0.048962783, -0.05156489, -0.17987493, -0.012037288, 0.06953726, -0.09458492, 0.1610021, -0.004063283, -0.032922342, 0.08995396, 0.1939926, -0.018710036, -0.08153231) * go_2(1.0, -1.0); result += mat4(-0.064830944, 0.06121252, -0.18886387, -0.12976822, -0.031117212, 0.12219633, 0.19070715, 0.12495262, -0.11994464, -0.24687837, -0.08425294, -0.016920334, -0.13286817, -0.3260188, -0.11776061, 0.1651019) * go_2(1.0, 0.0); result += mat4(-0.17652592, 0.002499805, -0.030541016, -0.01393431, 0.031418208, 0.08209422, 0.12430871, 0.4387016, -0.108871914, -0.09041422, 0.031226631, -0.1638517, 0.20756467, 0.014476537, -0.012701195, -0.03440563) * go_2(1.0, 1.0); result += mat4(0.005320072, -0.0032291536, -0.017209187, 0.031944863, -0.2479921, -0.24433962, -0.13832912, 0.07835928, -0.17707248, 0.028202811, -0.19121435, 0.164587, 0.123152815, 0.0050288937, 0.084104605, -0.0380019) * go_3(-1.0, -1.0); result += mat4(0.16008669, -0.018608516, -0.013778938, 0.033447385, -0.01242472, -0.070916265, 0.026909694, -0.07318777, 0.15158044, 0.12047607, -0.1709358, 0.2031767, 0.0025611701, -0.21457459, 0.2791286, 0.10159932) * go_3(-1.0, 0.0); result += mat4(0.14320926, 0.020023825, -0.0484187, 0.011563084, -0.2640472, -0.013056275, 0.004234292, -0.095376395, 0.28363484, -0.0058227647, -0.0777649, 0.05238444, 0.41757923, -0.07081097, 0.012567031, -0.13029522) * go_3(-1.0, 1.0); result += mat4(0.07266207, 0.042793367, -0.08212271, -0.23401663, -0.19457819, 0.4191269, -0.03095442, 0.15339781, -0.28451788, 0.09316364, 0.10231693, -0.22844811, 0.111623526, 0.120017685, 0.18777381, 0.014420896) * go_3(0.0, -1.0); result += mat4(0.15037206, -0.29763284, 0.2601235, 0.0193363, 0.13686465, 0.009907918, -0.37781665, 0.04916627, 0.14114739, 0.5043813, 0.0447959, -0.029427614, 0.041768756, 0.27211213, 0.14163221, 0.086162075) * go_3(0.0, 0.0); result += mat4(0.19159287, 0.21363218, 0.15053211, 0.08992885, 0.100828275, 0.09379921, 0.030783929, 0.11664482, -0.059145752, -0.19400764, -0.09351283, -0.016430443, -0.12910964, -0.067078374, 0.11760082, 0.121194765) * go_3(0.0, 1.0); result += mat4(-0.055059325, 0.09299572, 0.06848913, 0.06334532, -0.1476285, 0.111801244, -0.033960916, 0.06474366, -0.04952303, 0.27885208, -0.052447475, 0.09226763, -0.15024844, -0.0033919013, 0.013498364, 0.09135676) * go_3(1.0, -1.0); result += mat4(-0.017010042, -0.122343406, -0.19097193, -0.27957183, -0.18206005, 0.102321096, 0.22794476, 0.0439245, -0.23710132, -0.08070259, 0.17377135, 0.23811814, 0.17799385, 0.049567625, 0.1470908, 0.07329385) * go_3(1.0, 0.0); result += mat4(0.0038071256, 0.19454515, -0.01222965, -0.07390379, -0.0532754, 0.03942833, 0.123840906, 0.023459576, -0.0658742, -0.023957543, -0.14682837, 0.1221027, -0.010986398, -0.066184506, 0.03026491, -0.0638446) * go_3(1.0, 1.0); result += vec4(-0.06427697, -0.00039365015, 0.011889719, 0.060232002); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf1 //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.012110923, 0.07818654, 0.07964548, 0.11885079, -0.07694473, -0.01378252, 0.006632789, -0.12876098, 0.0069211307, 0.022278586, 0.069553085, 0.16569804, -0.11123615, 0.06125189, -0.11232848, 0.1559266) * go_0(-1.0, -1.0); result += mat4(-0.3261174, -0.25586754, 0.21129315, 0.3135101, 0.1509055, 0.0044283345, 0.024674175, -0.08000473, 0.01213029, 0.09093019, 0.04942677, 0.09806723, -0.16454464, -0.14433062, -0.058094524, -0.060819894) * go_0(-1.0, 0.0); result += mat4(0.023174008, 0.02858724, 0.07685972, 0.036857616, -0.10415571, 0.10241035, -0.01893166, 0.02065923, 0.058356714, 0.096426114, -0.03772327, -0.1529002, 0.13740575, -0.048291504, -0.06152548, -0.15199897) * go_0(-1.0, 1.0); result += mat4(0.029300174, -0.13222043, 0.0139825605, -0.02274408, 0.062944874, 0.028447356, 0.05960515, 0.034447193, 0.03133432, -0.019283533, -0.024591971, -0.0043914663, 0.15245225, 0.006851478, -0.051783554, 0.17453748) * go_0(0.0, -1.0); result += mat4(-0.09125915, 0.081739366, 0.01196335, 0.23130219, -0.22557035, -0.13537665, 0.0022028848, -0.043430023, 0.22759882, 0.07920754, -0.027986467, -0.14051494, -0.19557038, -0.03585936, -0.4258294, -0.03856216) * go_0(0.0, 0.0); result += mat4(0.18511422, -0.09368415, 0.1551229, 0.04322566, -0.023400841, -0.02261204, 0.15129441, -0.007954805, -0.10739125, 0.019459398, 0.013128325, 0.018073296, 0.20886365, -0.20662378, -0.03814699, -0.09272838) * go_0(0.0, 1.0); result += mat4(-0.027352437, -0.039882626, 0.12598103, -0.093930446, 0.030846786, -0.09325075, -0.009084744, -0.024584265, 0.07159868, 0.14162529, 0.19019091, 0.058855128, -0.09880401, -0.01843218, 0.14753596, -0.2449532) * go_0(1.0, -1.0); result += mat4(0.06565521, 0.09150168, -0.08654865, 0.0829788, -0.07596146, -0.01815166, -0.08786775, -0.03477514, 0.20538878, -0.012766377, 0.020719538, 0.088188395, -0.034300096, 0.29972988, -0.20005241, 0.018425167) * go_0(1.0, 0.0); result += mat4(0.11713916, 0.024167519, 0.05167596, -0.0027117804, -0.016994188, 0.048177514, -0.012556207, 0.010979094, 0.09098878, 0.028514355, 0.06063336, -0.06624107, 0.012754856, 0.013208708, -0.061374772, -0.0025992664) * go_0(1.0, 1.0); result += mat4(-0.09053513, 0.03183455, 0.017340872, 0.12934409, -0.022161964, -0.0015361432, -0.049972344, -0.12763855, 0.12779881, -0.04697911, 0.018968226, -0.119873665, 0.05462772, -0.13919477, -0.10226718, -0.2540179) * go_1(-1.0, -1.0); result += mat4(-0.29912186, -0.09291771, 0.050926663, 0.49361777, 0.21372582, 0.076717265, -0.058968987, -0.1572678, 0.3194591, -0.120582424, 0.03942037, 0.023128232, 0.24321598, 0.07046334, -0.21204855, -0.648296) * go_1(-1.0, 0.0); result += mat4(0.05366883, -0.020366706, 0.020979457, -0.06893884, 0.04837168, 0.017253762, 0.008874203, -0.020785445, -0.20425391, 0.060179923, 0.046167206, 0.09863377, -0.14381303, 0.038928367, -0.06590863, -0.18408588) * go_1(-1.0, 1.0); result += mat4(0.07099762, 0.2029403, -0.033945918, 0.15202214, 0.0901113, -0.27336198, -0.17693861, -0.16206753, -0.17642029, 0.09400492, -0.11165698, -0.07863893, -0.16306102, -0.056210615, 0.22173557, 0.013508989) * go_1(0.0, -1.0); result += mat4(0.08541511, -0.27093616, -0.35273993, -0.48919773, 0.038383547, -0.16013749, 0.012996215, -0.03434873, 0.07024113, -0.28971404, 0.10623425, -0.0019642068, -0.062374946, 0.3291145, 0.22468035, -0.42971882) * go_1(0.0, 0.0); result += mat4(0.020427933, 0.15062793, 0.08308975, -0.025095072, 0.030093266, -0.09649862, -0.03382388, -0.0016017791, 0.105402954, 0.020693144, -0.051065, 0.07704679, 0.02864139, -0.00135146, 0.03762216, 0.029277142) * go_1(0.0, 1.0); result += mat4(0.01700994, 0.12214317, 0.06749582, 0.07354159, -0.093085855, -0.065021954, 0.010773045, -0.00095128635, -0.045384295, -0.072611265, -0.043900184, 0.049471326, 0.029131187, 0.03180158, -0.13313527, 0.05280797) * go_1(1.0, -1.0); result += mat4(0.14751251, -0.15087761, 0.09932281, -0.099232934, -0.062390897, 0.112391844, -0.09159478, 0.15856399, 0.034708973, 0.01819943, -0.02730164, -0.13562973, -0.05687333, -0.0114601655, 0.07025971, 0.02496533) * go_1(1.0, 0.0); result += mat4(-0.0117268525, -0.026162883, 0.07481553, 0.13420302, 0.029870516, 0.07405776, -0.06379041, 0.09631234, -0.07754842, 0.035888605, 0.0034764851, -0.040771756, -0.092022054, -0.034230903, -0.02281844, -0.0028173258) * go_1(1.0, 1.0); result += mat4(-0.059846643, 0.016772347, -0.02287152, 0.07036337, -0.024946844, 0.09826078, -0.068491876, 0.20852126, 0.073890835, -0.058288682, 0.013093785, -0.05776076, 0.0516503, 0.052794468, 0.10837015, 0.038539834) * go_2(-1.0, -1.0); result += mat4(-0.16391893, -0.008062687, -0.35022175, 0.2510062, -0.15820411, 0.048403125, 0.024878092, 0.037888516, -0.035924178, -0.068953894, -0.025386479, 0.24405715, -0.018495679, -0.051277515, 0.14754932, -0.031538483) * go_2(-1.0, 0.0); result += mat4(-0.038429607, -0.047140498, -0.018157095, -0.029318782, -0.04094171, -0.11870087, 0.11214255, 0.07142628, 0.021007229, -0.005681072, 0.1662777, 0.10829575, 0.112268396, 0.03567479, -0.06738845, 0.0032037434) * go_2(-1.0, 1.0); result += mat4(-0.032217573, 0.2102397, -0.20617546, -0.07920811, 0.12918773, 0.054486286, -0.13656865, 0.05806265, 0.01963165, 0.049910642, 0.15538268, 0.10724465, -0.09697837, -0.03070673, -0.0071386313, -0.11899626) * go_2(0.0, -1.0); result += mat4(0.130827, 0.0051715383, -0.07212691, 0.45726067, 0.2773031, 0.2973666, 0.3951691, 0.01333662, -0.14561643, 0.04508669, 0.121690124, 0.13326228, -0.22579186, 0.058161184, 0.09281702, -0.00079749606) * go_2(0.0, 0.0); result += mat4(-0.00771113, 0.09912341, -0.41895548, -0.06705759, 0.029148718, 0.052991726, 0.18665347, -0.031787418, 0.23053595, 0.09444956, 0.10691037, -0.06325714, -0.05335701, 0.1917427, -0.0065284846, 0.032622546) * go_2(0.0, 1.0); result += mat4(-0.056801565, -0.019131258, -0.0939022, -0.08130343, -0.11051993, 0.0035269214, -0.047361933, -0.0543875, 0.10854369, 0.06445185, 0.016828364, -0.022595318, 0.1450623, 0.033027507, -0.020425137, 0.16169788) * go_2(1.0, -1.0); result += mat4(-0.08747717, 0.07770065, 0.018155783, 0.07160794, 0.09860347, -0.04329888, -0.0043579484, -0.2014418, -0.060260013, 0.0036374568, -0.17566042, -0.2268221, 0.001273691, -0.2609373, -0.19417606, -0.04102927) * go_2(1.0, 0.0); result += mat4(-0.086845055, -0.114253804, -0.13433142, -0.025941795, -0.0155711295, -0.13578776, 0.12059696, -0.08760523, -0.0057348222, 0.12164273, 0.07270617, -0.06352636, 0.08894258, 0.04140841, 0.1230304, -0.030357126) * go_2(1.0, 1.0); result += mat4(0.03320213, 0.015911903, -0.06288296, -0.121976145, 0.2713457, 0.13913193, -0.092420585, 0.105714336, 0.10294281, -0.04591945, -0.11767934, 0.032249406, -0.06506192, -0.04639334, 0.08137017, -0.031746846) * go_3(-1.0, -1.0); result += mat4(0.13717805, 0.0071242675, -0.077256985, -0.14974317, -0.08467893, -0.20126395, -0.06240603, 0.09554399, -0.075844854, 0.28380412, 0.046030026, 0.053188596, 0.50943077, 0.1179795, 0.32203588, -0.06712207) * go_3(-1.0, 0.0); result += mat4(-0.18528835, 0.0016975187, -0.0041140947, 0.11234392, -0.34049067, -0.056880493, -0.04325441, 0.09905571, 0.10978758, 0.009608353, -0.10801905, -0.04071131, -0.09096832, -0.12350487, 0.011801418, 0.22521795) * go_3(-1.0, 1.0); result += mat4(0.040283076, -0.034117915, -0.026142653, -0.06058959, 0.12511659, 0.4131219, 0.59190845, 0.39758852, 0.16032091, -0.5975032, -0.14516282, 0.115154505, 0.03874097, 0.18462797, 0.22934213, 0.05285643) * go_3(0.0, -1.0); result += mat4(-0.17804009, 0.33769128, -0.14572927, -0.029545018, 0.3897, -0.055615567, 0.15232995, 0.48788264, -0.21422523, 0.03397293, 0.0337794, -0.19830915, -0.022457365, -0.35096076, 0.42616987, -0.19268763) * go_3(0.0, 0.0); result += mat4(-0.13191561, -0.18337126, 0.017879983, -0.070472844, -0.09409196, -0.025770849, -0.060219247, 0.10869267, -0.17341033, -0.09199785, -0.0667796, -0.093538545, -0.21300837, 0.030474098, -0.04540468, 0.041321553) * go_3(0.0, 1.0); result += mat4(-0.0998177, -0.08669185, -0.0090886615, 0.0021083376, 0.08900095, 0.5062186, 0.45537788, 0.029077586, -0.1001008, -0.0077697043, -0.0096318, 0.11706454, 0.07401959, -0.00650215, 0.06092762, 0.037442297) * go_3(1.0, -1.0); result += mat4(-0.18500404, 0.0024998419, -0.11761331, -0.026825588, 0.27255726, 0.093010515, 0.3281413, -0.051473666, -0.050259475, -0.17258662, -0.23394547, 0.104795866, 0.035074063, -0.061560635, 0.05975411, -0.094255395) * go_3(1.0, 0.0); result += mat4(-0.023440497, -0.021479638, 0.0036277648, 0.004972212, 0.02416659, -0.09856867, -0.03971455, -0.27094853, 0.026615402, -0.0047890246, -0.13755885, 0.16591635, -0.0016293586, 0.133207, 0.047790572, 0.029041538) * go_3(1.0, 1.0); result += vec4(-0.0063728676, -0.029053684, -0.052831043, 0.006475641); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.0431447, 0.047972627, 0.09522898, 0.19048582, 0.0015511789, 0.1182684, -0.065335006, 0.061233886, -0.02451869, 0.065670215, -0.015341636, 0.06836347, 0.10215459, 0.17516296, 0.0857072, 0.072732896) * go_0(-1.0, -1.0); result += mat4(0.10117189, 0.049022958, -0.016017418, -0.12119866, 0.089112304, 0.016286526, -0.025251161, 0.03239003, -0.0783818, -0.086096615, -0.13673106, -0.15934734, -0.51308054, -0.061430074, -0.16208844, 0.2227776) * go_0(-1.0, 0.0); result += mat4(-0.011567444, 0.025550444, -0.018439503, -0.015003767, 0.11606929, -0.11613111, -0.040906087, -0.015202219, 0.03932618, -0.1106059, 0.03703376, 0.018548314, -0.12761284, -0.038109995, -0.23577367, 0.20272344) * go_0(-1.0, 1.0); result += mat4(0.025444161, -0.075270735, 0.10999789, 0.16305386, 0.016178958, -0.074034974, 0.1177035, -0.077481024, -0.047774278, -0.029782977, 0.23137823, -0.2389453, 0.033015423, -0.10381626, -0.16437943, 0.20906886) * go_0(0.0, -1.0); result += mat4(-0.098473966, 0.11013442, -0.18486807, 0.1907086, -0.17564997, -0.08509439, -0.42472756, -0.17446618, 0.3440862, 0.12719585, -0.12213955, -0.02246555, 0.18982963, 0.20809166, -0.36067408, 0.51116616) * go_0(0.0, 0.0); result += mat4(-0.019805575, 0.07812505, 0.061653323, -0.08379226, 0.026396899, 0.009063019, -0.10845824, 0.0827647, 0.045301896, -0.07748021, -0.07435832, 0.14860612, -0.077515624, 0.010588131, -0.22704287, 0.26849246) * go_0(0.0, 1.0); result += mat4(-0.02884339, -0.09512523, -0.038564682, 0.08862835, 0.041666254, -0.10532901, 0.040582962, -0.10063983, -0.15736029, -0.03644334, -0.005061672, 0.04302295, -0.046482194, -0.05262547, 0.05110866, 0.03204655) * go_0(1.0, -1.0); result += mat4(-0.005932702, 0.033263832, 0.0044865874, -0.02328917, 0.056534443, -0.14084046, 0.022353357, 0.015087431, -0.2734596, -0.026544483, 0.06297078, 0.11277746, 0.06127936, 0.02466357, -0.04970561, 0.02098484) * go_0(1.0, 0.0); result += mat4(0.013603583, 0.036264602, 0.10985147, 0.01532773, -0.09012781, 0.1132652, -0.17016481, 0.025332611, -0.077462606, 0.02990799, -0.10627784, -0.006231141, -0.089164406, -0.051507175, -0.043900985, 0.09049239) * go_0(1.0, 1.0); result += mat4(-0.15391691, 0.1915742, 0.014101639, -0.022153432, 0.06291936, -0.017871676, -0.016763045, -0.14741553, -0.011252563, -0.20720159, -0.030648025, -0.0142307645, 0.010291614, -0.09243969, -0.052940153, 0.0061574522) * go_1(-1.0, -1.0); result += mat4(0.032283742, 0.030768922, 0.1070225, -0.027818602, 0.10032608, 0.0061178426, -0.03561339, -0.26687133, 0.14369439, -0.11362691, -0.08980895, 0.066520914, 0.33414948, 0.006998835, 0.09193012, -0.2857383) * go_1(-1.0, 0.0); result += mat4(-0.059588976, -0.02046844, -0.042585023, 0.031939838, 0.12796514, -0.06155685, 0.03540324, 0.009929082, -0.0039611827, 0.10790477, 0.049435645, -0.083034374, 0.23874004, -0.07460337, -0.020173345, -0.2006587) * go_1(-1.0, 1.0); result += mat4(-0.13217632, 0.052319963, -0.026713084, -0.0051368694, -0.10380872, -0.28659084, 0.0044393227, 0.005174543, -0.05092618, -0.07092548, -0.027397033, -0.01609789, 0.13699281, -0.14706929, 0.17737861, -0.23746766) * go_1(0.0, -1.0); result += mat4(0.19268502, 0.14133929, -0.1305119, -0.4034132, 0.057504695, -0.24550998, -0.081932545, 0.45489246, -0.29331785, 0.19625074, 0.063166246, 0.15158689, 0.6715147, -0.4610189, 0.08921431, 0.17761138) * go_1(0.0, 0.0); result += mat4(0.044718128, -0.011809122, 0.024131307, -0.30093196, -0.05607289, 0.047759805, 0.004210022, 0.098192796, 0.030430846, 0.008207501, 0.12266905, -0.10549182, 0.11584339, -0.091016166, -0.08635591, -0.13889709) * go_1(0.0, 1.0); result += mat4(-0.19226642, 0.07147627, -0.14759602, 0.4041079, 0.0744628, -0.19612685, 0.1498252, -0.06273549, 0.017959936, 0.10858338, -0.14985329, 0.062042814, -0.13240446, -0.24362786, 0.113626175, -0.15332204) * go_1(1.0, -1.0); result += mat4(0.08383099, -0.13935047, -0.25981048, 0.16491203, 0.07513876, -0.28346774, 0.19722275, -0.044425573, 0.020889329, -0.22140723, 0.025403097, -0.09183192, 0.014202567, -0.18666178, 0.062913105, -0.047674105) * go_1(1.0, 0.0); result += mat4(-0.1862771, 0.25878942, -0.043018065, 0.22144824, 0.016088247, 0.12113542, -0.11965952, -0.01587184, 0.07830932, -0.16069177, 0.13421321, 0.018718706, 0.09548377, 0.018543294, 0.013614677, -0.1054485) * go_1(1.0, 1.0); result += mat4(-0.2121733, -0.015635416, 0.027564054, -0.085904464, 0.064805664, -0.070543915, 0.08966146, -0.06359783, 0.01131311, 0.046913184, -0.09809833, -0.092063695, -0.087217696, 0.012411829, 0.0045399712, 0.027389864) * go_2(-1.0, -1.0); result += mat4(-0.19307798, 0.09449126, 0.084036835, 0.30262446, 0.011706106, 0.029800637, 0.04612629, 0.006186647, 0.11228541, 0.055147965, 0.17659879, -0.023410015, 0.19965266, -0.06684007, -0.081968054, -0.052410994) * go_2(-1.0, 0.0); result += mat4(-0.058564443, 0.08252549, 0.058217794, 0.0864448, -0.25663558, 0.080260284, -0.0010294432, 0.05830051, -0.07684524, 0.1820709, 0.04438993, 0.019178499, -0.12425012, -0.04596089, -0.010032888, -0.0012803525) * go_2(-1.0, 1.0); result += mat4(-0.43352658, 0.15262963, 0.25620222, 0.22428556, 0.09667152, 0.0037820593, -0.07951691, -0.11553085, 0.12982155, 0.17988266, -0.14283511, 0.074744284, 0.03604327, 0.00452661, -0.12865154, -0.020020623) * go_2(0.0, -1.0); result += mat4(0.06850602, -0.18057181, 0.2093389, -0.07333886, 0.28406742, -0.048766967, 0.18114483, 0.47292945, -0.2340266, -0.06862712, 0.28263155, 0.3150323, -0.054724697, -0.16958356, 0.27928987, -0.19666018) * go_2(0.0, 0.0); result += mat4(0.03281329, 0.0038649621, -0.07108877, 0.10791149, 0.15235375, -0.3083721, 0.168294, 0.10379698, 0.029038485, 0.16282903, 0.04483725, -0.018684763, 0.108186625, 0.027885616, -0.019351846, 0.1623065) * go_2(0.0, 1.0); result += mat4(-0.110499054, 0.31347123, 0.030852, 0.01631416, -0.1466389, 0.080429435, -0.18689284, 0.10667815, 0.20645237, -0.18004708, -0.10570413, -0.15435064, -0.019000605, -3.126077e-06, 0.037761535, -0.015040956) * go_2(1.0, -1.0); result += mat4(-0.023364332, -0.023399066, 0.2712722, 0.049637552, -0.10222765, -0.2698945, 0.20991959, 0.04921932, 0.21510898, -0.0751939, -0.19781734, -0.28162366, -0.041881047, 0.0065111094, -0.04102195, 0.0982682) * go_2(1.0, 0.0); result += mat4(-0.032176614, 0.019144032, -0.08985387, 0.091637276, 0.1012352, 0.0003583357, 0.07897295, -0.09531175, -0.001155058, 0.074372366, -0.026186578, 0.07283374, 0.06052053, 0.009307753, -0.03874333, -0.06228009) * go_2(1.0, 1.0); result += mat4(-0.022224072, -0.15717922, -0.1406057, -0.05941157, -0.028769474, -0.21226564, -0.036570027, 0.22266355, 0.14120889, 0.014577123, 0.10216447, 0.018429281, 0.056729726, -0.055834044, 0.058146577, -0.11999068) * go_3(-1.0, -1.0); result += mat4(0.009995364, -0.020045493, -0.0057422677, 0.0643022, 0.016475432, -0.030856136, 0.042140726, 0.15077904, -0.32955253, 0.0694449, 0.17931722, 0.3439302, -0.12484157, -0.10958869, -0.15755124, -0.09755644) * go_3(-1.0, 0.0); result += mat4(-0.008314924, 0.07704758, 0.043228816, -0.08110893, 0.099286236, -0.053224478, 0.22877018, -0.189486, -0.00798416, 0.018341504, 0.10734141, 0.0752633, -0.042524844, -0.086395286, 0.14299925, 0.026488977) * go_3(-1.0, 1.0); result += mat4(-0.052531082, 0.19139186, 0.12205995, -0.2573172, 0.15157184, 0.0073150825, 0.089774385, 0.06604469, -0.16528498, -0.002511137, 0.14287429, -0.07819732, 0.025014274, 0.15338829, 0.0761692, -0.02803716) * go_3(0.0, -1.0); result += mat4(-0.21000335, 0.15277153, 0.08546171, 0.2816124, -0.16559112, -0.11068559, 0.47053605, -0.009787771, -0.0013089112, -0.06985127, 0.44743782, 0.25142467, -0.32670796, 0.044035822, -0.12545367, -0.2996084) * go_3(0.0, 0.0); result += mat4(-0.11526387, 0.15654811, 0.099616654, 0.15473685, 0.21278231, 0.046207245, 0.117993094, -0.26825273, -0.12539764, 0.14013724, 0.17357737, -0.05387817, 0.076738276, -0.13339446, 0.15005626, -0.2108176) * go_3(0.0, 1.0); result += mat4(-0.0008846504, -0.05998622, -0.028892396, 0.04784136, 0.0104263965, 0.10899508, -0.073364735, 0.077516064, -0.074248806, -0.21749993, -0.26203, 0.041161157, 0.09366407, -0.026498007, 0.0122177545, 0.03892727) * go_3(1.0, -1.0); result += mat4(0.04349908, 0.13671173, 0.2242545, -0.028021423, -0.03802222, 0.0052366396, -0.010709643, 0.031290106, 0.06291333, -0.024909683, -0.15439379, -0.04502091, 0.2062182, -0.5983536, -0.09670497, -0.38446042) * go_3(1.0, 0.0); result += mat4(-0.008962513, 0.13044207, 0.04964221, 0.012250417, 0.012129821, 0.019985713, -0.06421885, 0.009168735, -0.044516414, 0.071368866, -0.006634213, 0.06497366, 0.08578495, -0.10586125, 0.06628038, -0.14006054) * go_3(1.0, 1.0); result += vec4(0.056541316, 0.041788545, -0.036094554, -0.021763096); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf1 //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.0647927, 0.053666476, -0.14723225, 0.027874574, -0.0003166473, 0.07337155, -0.061972085, -0.012667777, -0.17071614, 0.091927536, -0.051160213, 0.21336353, 0.13854574, 0.09582817, 0.032316446, 0.13838023) * go_0(-1.0, -1.0); result += mat4(-0.0398984, 0.108049214, 0.093780346, -0.022015186, -0.15188989, -0.1381083, 0.2998843, 0.21623154, -0.08862326, 0.025862623, 0.06895634, 0.13529755, 0.06957801, -0.0011681129, 0.105972745, -0.04722446) * go_0(-1.0, 0.0); result += mat4(-0.026321493, -0.04828038, -0.012545767, -0.005490858, -0.054038163, 0.075943105, -0.11526662, 0.022242405, -0.03543104, -0.12451852, -0.14911178, 0.013503498, 0.08773292, 0.09695139, -0.013498657, -0.27424073) * go_0(-1.0, 1.0); result += mat4(0.018575635, -0.11321618, -0.07853153, 0.04104883, 0.0018416744, 0.11579002, 0.03685964, -0.031546146, -0.1755398, 0.23517849, -0.08095411, 0.031999595, -0.18542038, -0.26171613, -0.20567231, -0.05683613) * go_0(0.0, -1.0); result += mat4(0.1538556, 0.21723682, 0.12131733, -0.15308167, 0.103326, -0.006956118, 0.043583486, -0.23811384, -0.103285454, 0.05543916, -0.37894246, 0.32072112, 0.22651967, 0.03516268, 0.34612176, 0.23688535) * go_0(0.0, 0.0); result += mat4(0.040021293, 0.0029912095, 0.04885362, 0.061496444, 0.016926387, -0.118446946, 0.038948335, -0.0934512, -0.25194243, -0.054018084, -0.07149527, 0.017903058, 0.0845516, 0.33802906, 0.11953944, -0.081294954) * go_0(0.0, 1.0); result += mat4(-0.09558082, -0.36974236, -0.07524102, 0.11131445, 0.047626104, 0.12854609, -0.10264962, -0.044669047, -0.05572307, 0.34475142, -0.16806377, -0.0037204176, 0.03400533, -0.04047774, 0.024379745, 0.09056291) * go_0(1.0, -1.0); result += mat4(-0.039392482, 0.2553437, 0.11705501, 0.03219211, 0.073977776, -0.16610906, -0.032796364, -0.054669864, -0.07123178, 0.00079619256, -0.36920992, -0.029054813, 0.12830003, 0.004987549, 0.08724278, -0.029499404) * go_0(1.0, 0.0); result += mat4(0.021272454, -0.063295126, 0.011779576, 0.103093, -0.011095461, 0.027948728, -0.014605259, -0.04723974, -0.05334346, -0.044831257, -0.07296399, -0.03314197, -0.01687865, -0.09261895, -0.06128567, 0.092708185) * go_0(1.0, 1.0); result += mat4(0.0077418387, 0.00871427, 0.060824487, 0.1093608, -0.021077013, -0.057341542, -0.04769576, -0.08144089, 0.0212823, -0.06731425, -0.04134463, -0.0016761447, -0.03402026, 0.036424547, 0.11689576, -0.14946719) * go_1(-1.0, -1.0); result += mat4(0.18536687, 0.020073935, 0.17041959, 0.024790209, 0.08397728, -0.13884324, 0.013950321, -0.055075396, -0.09317963, -0.05723721, -0.060491834, 0.0017911601, -0.109154835, 0.010338362, -0.1982491, -0.21752335) * go_1(-1.0, 0.0); result += mat4(0.031852514, 0.031424347, 0.07817056, 0.07770759, 0.019805199, -0.091223724, 0.11914662, 0.1673029, -0.018734453, 0.16275099, 0.23245652, 0.36139074, -0.1396047, -0.14774057, 0.13756078, -0.123794965) * go_1(-1.0, 1.0); result += mat4(-0.034937833, 0.20777488, 0.10104809, -0.035140667, 0.2536575, 0.010970045, 0.16896339, -0.081219964, -0.062478427, -0.0010431948, -0.027980985, 0.11446318, -0.127309, 0.21002083, 0.044436257, -0.16986957) * go_1(0.0, -1.0); result += mat4(0.06309646, -0.042341243, 0.36642808, 0.18653205, 0.06973023, 0.06315932, -0.323688, 0.25672218, 0.042820994, 0.13792914, -0.12892757, -0.09220378, -0.18939693, 0.03862022, -0.17376114, -0.24673308) * go_1(0.0, 0.0); result += mat4(-0.02130602, -0.35428852, -0.011634983, -3.9823462e-05, 0.110818714, -0.2981158, 0.060209107, 0.012538829, -0.0744833, -0.050204318, -0.12676497, -0.031484153, -0.28799182, 0.22338839, -0.070876874, -0.02102363) * go_1(0.0, 1.0); result += mat4(-0.07929991, 0.014598492, 0.23034762, 0.024872296, 0.07480494, -0.17139243, -0.014421178, 0.056448363, -0.028626937, -0.022152562, 0.044871796, -0.048653606, 0.009350802, 0.019022083, -0.08554845, -0.0922645) * go_1(1.0, -1.0); result += mat4(-0.027405115, 0.1831188, 0.28516722, 0.19882526, 0.27299204, -0.06910511, 0.03244419, -0.0031333128, 0.061055277, -0.114398144, 0.03729459, -0.07840815, -0.37776002, -0.24129418, -0.54815483, -0.2702045) * go_1(1.0, 0.0); result += mat4(0.053723935, 0.13472083, 0.09563273, 0.19009806, -0.18722993, -0.25939655, -0.016197463, -0.067061596, 0.1647598, 0.061905228, 0.06191816, -0.018582113, -0.07218153, 0.11278394, 0.05478068, -0.104871586) * go_1(1.0, 1.0); result += mat4(0.0036616288, -0.045782693, -0.226954, -0.05043515, -0.078096785, -0.036197383, 0.09269631, 0.016823346, -0.0060579977, -0.041455746, 0.09032774, -0.09217121, 0.058089796, 0.060311552, 0.033079024, 0.022586476) * go_2(-1.0, -1.0); result += mat4(0.0436363, -0.079482526, 0.0027447809, 0.039558932, 0.13275702, 6.898711e-05, -0.21961488, -0.11315821, 0.0076181027, -0.025279062, -0.15829584, -0.063141204, 0.062049046, 0.13117202, -0.02435016, 0.109555416) * go_2(-1.0, 0.0); result += mat4(-0.010148116, 0.056620967, -0.015910713, -0.07370375, 0.1529919, 0.005792597, 0.02771225, -0.17027487, 0.096740395, 0.063347995, 0.17823112, 0.054105148, 0.04995114, -0.28613812, 0.06369567, 0.15978208) * go_2(-1.0, 1.0); result += mat4(-0.13688345, 0.16967694, -0.061759472, 0.013682004, -0.1290496, 0.07167547, -0.065592445, -0.17897636, 0.057080988, 0.035630587, 0.09140394, -0.08695068, 0.16807681, 0.014749346, 0.07875138, 0.034913708) * go_2(0.0, -1.0); result += mat4(-0.098915346, -0.31459075, -0.10892429, 0.1557498, -0.19764107, -0.26881596, -0.03589311, 0.45288458, -0.34171388, 0.12675741, 0.18415868, -0.19770056, 0.29025507, -0.15812592, 0.09685835, 0.0027761247) * go_2(0.0, 0.0); result += mat4(0.06425249, -0.01169722, 0.06379363, 0.053835012, -0.07356561, -0.06367294, 0.108630784, -0.14137438, 0.08536725, -0.03209748, 0.07250959, -0.014214082, 0.07170588, -0.25647813, 0.1092683, 0.18791042) * go_2(0.0, 1.0); result += mat4(-0.023783233, 0.14261739, 0.102011986, -0.03633555, -0.05032627, 0.09378387, 0.11764051, 0.1353335, 0.032817088, -0.1352964, -0.00667997, -0.13388929, 0.022861317, 0.0037358075, 0.018605746, -0.0009892831) * go_2(1.0, -1.0); result += mat4(0.22419162, -0.23105696, -0.09900454, -0.15831396, 0.12398773, 0.097933106, -0.13189293, 0.1330756, -0.19673057, -0.037342317, -0.13462654, -0.08974021, 0.030326528, -0.0815862, -0.118352115, 0.009187904) * go_2(1.0, 0.0); result += mat4(-0.012130391, -0.06408448, 0.13710785, -0.06678414, -0.09970725, -0.14895032, -0.02366641, 0.029581001, -0.07101809, 0.09414698, 0.018300869, 0.009139046, -0.0027311493, -0.2359952, -0.011602826, -0.007582444) * go_2(1.0, 1.0); result += mat4(-0.15473361, -0.06868751, -0.030721204, -0.08650113, 0.071349874, -0.08177769, 0.1611948, 0.18305337, -0.0144878505, 0.10975452, -0.026968453, -0.04909913, -0.059665974, 0.056036238, -0.11623168, -0.10584912) * go_3(-1.0, -1.0); result += mat4(-0.096973225, 0.054132458, -0.010600018, 0.089397885, -0.0031138035, 0.037452973, 0.041115325, 0.1924831, 0.14759748, 0.032560788, -0.082884625, 0.0324635, -0.083511285, -0.050381303, 0.025589975, -0.0981257) * go_3(-1.0, 0.0); result += mat4(-0.09183111, 0.034952193, -0.048511654, 0.020719057, 0.1863456, 0.01902738, 0.14455654, -0.008500172, 0.16385981, -0.07806569, -0.031216217, -0.17002788, -0.08882952, 0.07335293, -0.2223089, 0.01706056) * go_3(-1.0, 1.0); result += mat4(-0.08361569, 0.046698716, -0.016646344, 0.09351987, 0.0054158634, -0.13641126, -0.12396605, 0.011380122, 0.040951792, -0.11222528, -0.0031548145, -0.0022303525, 0.0350846, -0.03280425, -0.09972476, -0.113325305) * go_3(0.0, -1.0); result += mat4(-0.19961461, -0.27561286, -0.12783135, -0.062596925, 0.005870981, -0.24796526, 0.18717633, -0.16945636, -0.076396205, -0.08411448, 0.13751988, 0.21014418, -0.008655945, -0.09848541, -0.14536901, -0.2132181) * go_3(0.0, 0.0); result += mat4(0.14118621, 0.20831147, -0.020545695, 0.008340737, 0.016840864, -0.16912372, -0.121718146, 0.15108089, -0.19803092, -0.07827729, -0.047639225, -0.12277847, 0.04974115, -0.09349339, -0.2756667, -0.19581003) * go_3(0.0, 1.0); result += mat4(-0.0036992705, 0.16539848, 0.022026122, 0.07740234, -0.035687633, -0.004568715, 0.017408118, -0.09757294, -0.094941914, -0.3381112, -0.12724453, 0.025583982, -0.18571027, 0.047607586, -0.0704089, -0.055323426) * go_3(1.0, -1.0); result += mat4(0.13821335, 0.028168043, 0.09990671, -0.032266147, -0.067236245, 0.11512147, -0.112986445, -0.10818019, -0.10062181, 0.21276556, 0.01681818, 0.069806606, 0.09628121, 0.06456379, 0.10394843, -0.02343886) * go_3(1.0, 0.0); result += mat4(0.041937463, 0.072631165, 0.045366894, -0.0046993676, 0.03946691, 0.121010706, -0.030089365, -0.007266469, 0.0092267515, 0.14853416, -0.033248078, -0.027284347, -0.10031526, 0.15864117, -0.16782752, -0.18466589) * go_3(1.0, 1.0); result += vec4(0.07722432, -0.025165567, 0.034291282, -0.09902708); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.004729794, -0.0124398535, -0.08538641, -0.058604605, 0.008671952, 0.25604513, 0.020800482, 0.24144122, -0.028920606, -0.04705229, 0.030192787, 0.0010597534, 0.017666103, 0.0041322373, 0.20027764, 0.08919112) * go_0(-1.0, -1.0); result += mat4(0.0001626656, 0.05816014, -0.0060765734, 0.08811165, 0.35835367, -0.016291425, -0.56892496, 0.083845764, 0.15026698, -0.15916558, 0.08069463, -0.3931291, -0.0123534845, -0.111639686, -0.14637001, -0.08171439) * go_0(-1.0, 0.0); result += mat4(-0.114976816, 0.023376396, 0.13855027, 0.07438716, -0.069991484, 0.20377779, 0.23929878, -0.040769435, 0.018832395, 0.005638609, -0.091848075, 0.027843866, 0.023744943, -0.06620523, -0.11678267, 0.0844119) * go_0(-1.0, 1.0); result += mat4(0.0035854098, -0.08432094, -0.17799544, -0.10041983, 0.25605857, 0.021009786, 0.030499447, -0.09928291, 0.052178737, -0.08286175, -0.057888374, 0.024606042, 0.046342995, 0.13875343, 0.11279266, 0.19826262) * go_0(0.0, -1.0); result += mat4(-0.016232021, -0.21539623, 0.0936961, 0.021143785, 0.094262615, 0.049040064, 0.40978724, 0.15347758, 0.08884813, -0.24887115, -0.14756748, -0.5020875, 0.112477, 0.1466549, -0.33418837, 0.5769466) * go_0(0.0, 0.0); result += mat4(-0.16832942, -0.07354198, -0.12081261, -0.055348314, 0.39716053, 0.25583258, 0.09870877, 0.2151021, -0.025700683, -0.1801462, -0.04616654, -0.02782245, -0.054461803, -0.00042802413, -0.00163228, -0.004240747) * go_0(0.0, 1.0); result += mat4(-0.05193433, -0.0018198475, -0.17647028, -0.19462106, 0.1538165, 0.054894235, 0.12183955, 0.07340974, -0.0019901982, 0.0357373, -0.07597063, -0.06681543, -0.00090057997, -0.053894397, -0.010301875, -0.16553953) * go_0(1.0, -1.0); result += mat4(-0.30873474, -0.2836045, 0.057037193, -0.5016378, 0.11952749, 0.102353275, 0.2351629, -0.14635189, -0.019398788, -0.08776502, 0.021669978, -0.089918956, -0.2187901, -0.1180891, -0.049789533, -0.16109149) * go_0(1.0, 0.0); result += mat4(-0.078335494, -0.08867304, 0.03349591, -0.1000293, -0.20235832, 0.22917585, -0.09905303, 0.08381748, 0.014350217, -0.14478815, -0.027479894, -0.026432173, -0.10309177, -0.09860884, -0.019177807, -0.06963025) * go_0(1.0, 1.0); result += mat4(0.008169383, 0.12532842, -0.23369955, 0.077973194, 0.09076616, -0.021277165, 0.1721421, -0.26914293, -0.014729218, -0.023279984, -0.057670787, 0.003598546, -0.015225789, -0.0115396585, -0.26196182, -0.10724508) * go_1(-1.0, -1.0); result += mat4(0.16542235, 0.06589374, 0.07410237, 0.26753154, -0.3356288, 0.3096256, 0.07112498, -0.0992165, 0.15020338, -0.11021673, 0.18803611, 0.12918204, 0.109007336, -0.031968266, 0.057093572, 0.035949256) * go_1(-1.0, 0.0); result += mat4(0.065006174, 0.031055925, 0.0390232, -0.01678507, -0.21553491, 0.14171642, -0.19541772, -0.033691674, -0.06241631, 0.07497651, 0.024557155, 0.056778047, -0.060191352, -0.0261998, 0.07493729, -0.0699132) * go_1(-1.0, 1.0); result += mat4(-0.008541382, 0.020270415, -0.027760057, -0.040962905, -0.26732433, 0.34379438, -0.23012447, 0.0051356517, -0.04059567, 0.0972959, 0.039965224, -0.14796777, -0.0016924662, -0.116963714, -0.026353523, -0.29799464) * go_1(0.0, -1.0); result += mat4(0.03329303, -0.12663862, -0.0004959157, -0.11162377, 0.26238343, 0.43260252, -0.16504994, 0.10727678, -0.22505566, 0.43474057, 0.43304008, 0.05143919, 0.40494493, 0.08689636, -0.035733614, 0.25727916) * go_1(0.0, 0.0); result += mat4(0.12175736, -0.014467151, -0.17461288, -0.18480565, -0.26439998, 0.307935, -0.058916792, -0.014292711, -0.0569471, 0.10751278, -0.04134206, 0.1847734, -0.07519831, -0.033909313, -0.05001451, -0.136606) * go_1(0.0, 1.0); result += mat4(0.1424893, -0.026820501, 0.19645774, -0.0011315406, -0.14680974, 0.07662838, 0.21108222, 0.13260938, 0.17923595, -0.085527614, 0.08217639, 0.06579479, 0.05985784, -0.09016323, 0.11172888, 0.111903176) * go_1(1.0, -1.0); result += mat4(0.19842595, 0.0093640275, 0.10433465, 0.13341904, -0.082806975, 0.22555825, -0.1315717, 0.11907785, 0.24012424, 0.47776055, 0.1835734, 0.17483878, 0.079803735, 0.01155073, -0.21146573, -0.16484722) * go_1(1.0, 0.0); result += mat4(0.15064004, 0.021381427, 0.18301587, 0.21225913, 0.054995645, 0.03212186, 0.052798916, -0.048424408, 0.03609021, 0.0964704, -0.059469886, -0.05133066, -0.08157349, 0.051145166, -0.09107608, -0.1362262) * go_1(1.0, 1.0); result += mat4(0.090521574, -0.014747857, -0.081675015, -0.118686825, 0.04848682, -0.033071827, 0.008534588, 0.023765508, 0.16849907, -0.21797262, -0.17049783, -0.07824179, -0.033794608, 0.052612655, 0.095820345, -0.07262317) * go_2(-1.0, -1.0); result += mat4(0.22816367, -0.13772108, -0.036353834, -0.47638395, -0.0530902, 0.14089061, 0.076203234, 0.18006112, 0.121814854, -0.20750527, 0.08266107, -0.28634354, 0.14301859, -0.13458411, 0.00501663, -0.039783802) * go_2(-1.0, 0.0); result += mat4(-0.103384845, -0.14389835, 0.08275834, -0.068423435, 0.22643796, -0.02966374, -0.2847584, 0.037081387, 0.02349005, -0.19353923, -0.00095957273, -0.13623689, -0.073120415, 0.03941467, 0.21864155, -0.014019576) * go_2(-1.0, 1.0); result += mat4(-0.082576886, 0.17085212, 0.08971252, -0.04213377, -0.032548156, 0.022137715, 0.08399252, -0.0011743539, -0.09410863, -0.41728264, -0.20709297, -0.18933547, 0.027059928, 0.09743364, 0.2504647, -0.041173562) * go_2(0.0, -1.0); result += mat4(-0.20924084, 0.291118, 0.029851688, 0.16953468, 0.02936709, 0.12213576, 0.22944322, 0.108747594, 0.0001881129, -0.27398208, -0.009702691, 0.15449248, -0.9472944, -0.26114875, -0.28161275, -0.3495961) * go_2(0.0, 0.0); result += mat4(-0.12994622, -0.2758638, -0.1091727, -0.0968308, -0.14323105, 0.035175014, -0.08023811, 0.006023802, -0.031529594, -0.1486306, -0.3398172, -0.23240276, -0.29163983, 0.173475, 0.18809283, 0.22197202) * go_2(0.0, 1.0); result += mat4(0.048254848, -0.083444916, -0.014334202, 0.060992356, -0.023099286, -0.09492961, 0.05592045, 0.0026059286, 0.08998117, -0.108810075, -0.053304546, 0.045926623, 0.068255246, 0.099023566, 0.01595483, 0.1336309) * go_2(1.0, -1.0); result += mat4(0.21916585, 0.2837387, 0.14624594, 0.18843961, -0.06747584, 0.054924384, -0.082568415, 0.05011459, 0.014297759, -0.3884833, -0.054417178, -0.18970548, 0.088336475, -0.030646667, -0.2980552, -0.030035203) * go_2(1.0, 0.0); result += mat4(-0.02748568, -0.011897529, -0.2370837, -0.016740574, -0.0282112, 0.050353892, -0.10761107, -0.00036999505, 0.037646662, -0.17742962, 0.06489219, -0.158852, -0.08016933, 0.07808515, -0.105895035, 0.079869986) * go_2(1.0, 1.0); result += mat4(-0.0058994526, -0.037170693, 0.2574696, 0.06199102, -0.04497728, -0.10667442, -0.15183865, 0.0212881, -0.030842574, 0.073473394, 0.010764398, -0.00084518327, -0.03893014, -0.009649613, 0.07443129, 0.15108284) * go_3(-1.0, -1.0); result += mat4(0.11325495, -0.096435815, -0.097331434, -0.049700152, -0.17231967, 0.047090057, -0.019111065, 0.104790315, -0.15004838, 0.13950798, 0.055996202, -0.070548095, 0.047154237, -0.007650949, -0.053611025, -0.012242293) * go_3(-1.0, 0.0); result += mat4(0.12787002, -0.04958212, 0.053988468, 0.0017896162, 0.049493514, -0.009475431, -0.0022641935, 0.03933694, -0.005174597, 0.043754533, -0.1432976, 0.037084177, -0.04601288, -0.032077815, -0.059897035, 0.12584484) * go_3(-1.0, 1.0); result += mat4(0.019409029, 0.10492923, 0.268368, 0.12597778, -0.17733063, -0.0085961, -0.27136415, -0.049664587, 0.012515404, -0.21444482, -0.39275557, -0.12297177, 0.06800057, 0.19228315, 0.06245887, 0.35772634) * go_3(0.0, -1.0); result += mat4(-0.16317715, 0.2288402, -0.23235172, 0.22230752, -0.1646375, 0.13366091, 0.16681044, -0.17399235, 0.33997267, -0.3179832, -0.34756508, 0.39843196, -0.10748536, 0.322923, 0.23339489, 0.08684083) * go_3(0.0, 0.0); result += mat4(0.02835275, 0.12314228, 0.24030593, 0.30856124, 0.055735108, -0.044914473, 0.0031432225, 0.07469899, 0.1778018, 0.107083894, -0.023706734, -0.15501897, 0.0943098, -0.034707237, -0.18622099, 0.05257965) * go_3(0.0, 1.0); result += mat4(0.042839274, 0.12597966, 0.08979042, -0.0647561, -0.050434645, 0.049438696, -0.20008127, -0.05572608, 0.046238814, 0.12622325, -0.019017145, -0.13960391, -0.040050175, 0.14298008, -0.20270552, 0.13391526) * go_3(1.0, -1.0); result += mat4(-0.0073277587, 0.10606624, -0.08940439, -0.09656414, 0.12387374, -0.0013147948, 0.23607181, -0.00037969893, 0.050353236, -0.17266603, 0.27796733, -0.09877832, 0.02711225, 0.096394345, 0.07457944, 0.21541388) * go_3(1.0, 0.0); result += mat4(-0.18612787, -0.00027517386, -0.17136407, -0.06413671, 0.025629476, -0.04570916, 0.0008431566, -0.03419168, 0.08123608, 0.09465922, 0.11975521, 0.1269741, 0.08413221, 0.12125001, 0.04727287, 0.072378494) * go_3(1.0, 1.0); result += vec4(0.04244928, -0.014280219, 0.017129054, -0.08807801); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf1 //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.01973856, -0.05053795, 0.015545361, 0.10867395, 0.33441806, 0.14731607, 0.6793983, -0.21394718, -0.00846322, 0.09146322, -0.07427475, -0.078477465, -0.090998545, 0.133366, 0.105515696, -0.13784988) * go_0(-1.0, -1.0); result += mat4(-0.05404873, 0.09784018, -0.1337389, -0.18082313, 0.13461179, -0.3816801, 0.12209786, 0.08176651, 0.10461896, -0.43315184, 0.017470734, 0.20423968, -0.03941875, -0.101959296, -0.09440259, 0.09154717) * go_0(-1.0, 0.0); result += mat4(0.17229515, -0.06907825, -0.008382803, -0.16671611, -0.01576541, 0.03985307, 0.08209482, -0.11707446, -0.11793074, 0.13702396, -0.02013158, 0.07302033, -0.022301994, -0.11464677, 0.036753565, -0.093276784) * go_0(-1.0, 1.0); result += mat4(-0.017650167, 0.009475923, -0.17856382, 0.15925962, 0.06434641, -0.15568036, 0.038135886, 0.18855911, -0.04427734, 0.1878215, 0.10856261, 0.0041275816, -0.12046199, 0.13610138, 0.3741596, -0.12934728) * go_0(0.0, -1.0); result += mat4(-0.24631616, 0.0169485, -0.035534818, 0.37795424, -0.08546174, 0.07817259, 0.42897213, -0.47965595, -0.0146556785, -0.20510523, -0.18889453, 0.06476019, 0.1021008, -0.35398817, -0.031071864, -0.21416448) * go_0(0.0, 0.0); result += mat4(0.32810766, 0.050585747, -0.17658374, -0.13881154, 0.16417882, -0.21286008, -0.106835455, -0.1722344, -0.14151084, 0.08962986, 0.057395387, -0.01623662, 0.02570415, 0.15626897, -0.12687978, 0.080729105) * go_0(0.0, 1.0); result += mat4(-0.050597478, -0.018753758, -0.036346875, -0.017908493, 0.058593344, 0.008303028, 0.05254987, -0.06635018, -0.022532012, 0.029511122, 0.026682215, -0.054647952, 0.069466785, -0.08892492, 0.025351115, -0.023130694) * go_0(1.0, -1.0); result += mat4(0.2412473, -0.16138165, -0.15117447, 0.11851003, -0.096868426, 0.082690425, 0.27923304, 0.11590443, 0.19363573, -0.15770023, -0.066793665, 0.011681678, 0.14037277, -0.112065665, -0.048159517, 0.009453693) * go_0(1.0, 0.0); result += mat4(0.1580054, -0.0060506654, 0.05267837, -0.09178131, -0.09107123, 0.23191126, 0.21108283, -0.070422985, 0.024321035, 0.06131459, 0.066626504, 0.032481454, 0.044402298, 0.1390604, -0.14432502, 0.040869843) * go_0(1.0, 1.0); result += mat4(0.10264861, 0.013504324, 0.012482852, -0.1781206, -0.12799414, -0.27026084, -0.123830505, 0.098105, -0.039127555, 0.09367889, 0.122323096, 0.1416734, 0.044763107, -0.21801683, -0.14018978, 0.17646866) * go_1(-1.0, -1.0); result += mat4(0.017453065, 0.11498537, -0.10998983, -0.3116098, -0.3099762, 0.5024706, 0.051817298, 0.03170681, -0.18937826, 0.07946567, -0.11978771, -0.09523745, -0.0033551592, -0.11768945, 0.08932359, -0.06689581) * go_1(-1.0, 0.0); result += mat4(0.1507582, -0.013266159, -0.073085934, -0.07252967, -0.06301927, -0.13218755, 0.12984878, -0.13678701, 0.023422396, 0.082123175, 0.006906731, -0.004018426, -0.15813835, 0.13711788, 0.016018609, 0.13443229) * go_1(-1.0, 1.0); result += mat4(-0.06960673, 0.16156524, -0.1374069, -0.05803206, -0.077960715, -0.10676749, 0.26282015, 0.03521529, 0.058099385, -0.014738148, 0.0011174522, 0.24279532, -0.023991548, -0.108812414, -0.08886019, 0.20584475) * go_1(0.0, -1.0); result += mat4(-0.08043308, 0.063343, 0.055290066, -0.15991378, -0.08096304, -0.23888679, 0.019161629, 0.38381267, 0.3672934, -0.119608454, -0.43623593, -0.46014485, -0.5323366, 0.1318621, 0.087373205, -0.05535459) * go_1(0.0, 0.0); result += mat4(0.20640239, -0.1369444, -0.21677823, 0.08202178, 0.10515278, 0.06810837, 0.073207974, 0.23623931, 0.102422275, -0.05016664, -0.0039228587, -0.1810343, -0.2235563, -0.1246854, 0.1428113, -0.10609135) * go_1(0.0, 1.0); result += mat4(-0.031941894, -0.08905056, 0.21501167, 0.11244667, -0.011811734, 0.21630247, 0.07589472, -0.040489636, -0.11824066, -0.11520391, -0.10075633, -0.035642453, 0.062144946, 0.0073282206, 0.14119269, -0.060479023) * go_1(1.0, -1.0); result += mat4(-0.29382935, -0.056808118, 0.051812876, -0.061358813, -0.08344258, 0.124203674, 0.037964176, -0.01961274, -0.000951725, 0.50005037, -0.24176972, 0.06487161, -0.15469861, 0.04336187, 0.17826353, 0.040010225) * go_1(1.0, 0.0); result += mat4(0.02044482, -0.0879271, -0.01053958, -0.31148303, 0.07497373, -0.11548258, -0.1666126, 0.02369657, -0.058044076, 0.010801491, -0.005933901, -0.08910467, 0.007953008, 0.03761974, -0.029501524, 0.16816042) * go_1(1.0, 1.0); result += mat4(0.1779597, -0.10213089, 0.29942423, -0.016642543, -0.015537001, -0.04676146, 0.09585872, -0.0055750017, -0.014361908, -0.20667697, -0.11348746, 0.13081487, -0.10437329, 0.14328459, 0.11648822, -0.09163837) * go_2(-1.0, -1.0); result += mat4(0.019033967, -0.12420627, -0.07748253, 0.43203858, -0.109799065, 0.07605535, 0.060791396, -0.24517195, -0.15674245, 0.21267459, 0.10665515, -0.073150024, -0.1358355, 0.0054066703, -0.16434059, -0.06031853) * go_2(-1.0, 0.0); result += mat4(-0.18834068, 0.26840356, -0.12937617, 0.16103932, -0.0062331813, -0.13630053, -0.013911821, 0.022389365, -0.044232946, -0.056454606, 0.022426741, 0.18010215, 0.041900013, 0.03375041, -0.11376866, -0.010313381) * go_2(-1.0, 1.0); result += mat4(0.12497669, -0.31161824, 0.097568035, 0.19443443, -0.05056519, -0.0031457904, 0.1055554, -0.083650924, 0.07630523, -0.34177595, -0.093093194, 0.20701368, -0.030962149, -0.054470222, -0.23853977, 0.004326528) * go_2(0.0, -1.0); result += mat4(0.34370202, 0.085750066, -0.16071722, -0.54335934, -0.35595295, -0.050744478, -0.17405547, 0.008628697, -0.007086256, 0.23164117, 0.340156, 0.5475976, -0.15292351, 0.28019544, 0.038059216, 0.0044727) * go_2(0.0, 0.0); result += mat4(-0.08231968, -0.0052294536, 0.07451547, 0.22278999, -0.3305531, 0.0017458396, 0.10818422, -0.21325395, -0.08807993, -0.110342845, 0.10082142, -0.051594347, 0.24192205, -0.18042035, -0.0095462985, -0.08757798) * go_2(0.0, 1.0); result += mat4(0.096379586, 0.021887815, -0.05097233, -0.06797989, -0.026171045, 0.022944937, -0.015915364, 0.037667938, 0.17216732, -0.014889412, 0.07343887, 0.028236505, 0.0015047621, 0.1355103, -0.09918284, -0.07673695) * go_2(1.0, -1.0); result += mat4(-0.25385055, 0.15163356, 0.0030003798, 0.18464413, 0.05611221, 0.099498056, -0.07128191, 0.042955168, 0.027493173, 0.07440157, 0.07814497, 0.096160784, 0.13571084, 0.056412842, -0.031997006, -0.16073681) * go_2(1.0, 0.0); result += mat4(-0.21634746, 0.025153082, -0.064477116, 0.0005679147, -0.0029436245, 0.12794618, 0.024849026, 0.03018052, 0.11723976, 0.059955597, -0.013594654, 0.09091745, 0.04775348, 0.21260159, -0.07463213, -0.06727042) * go_2(1.0, 1.0); result += mat4(-0.12166018, 0.024545137, 0.08611618, -0.17627168, 0.09042604, -0.14157623, -0.22147785, 0.09100581, 0.11078359, 0.031410985, -0.17170976, 0.09532806, -0.059569277, 0.09392676, 0.11784347, -0.21471368) * go_3(-1.0, -1.0); result += mat4(0.1483187, -0.2217563, 0.12032977, 0.14932398, 0.27428308, -0.04568031, 0.12670338, 0.09586169, 0.06700745, 0.005126449, 0.0027694793, -0.033667028, 0.06447861, -0.08585174, -0.05509812, -0.11358761) * go_3(-1.0, 0.0); result += mat4(-0.22750492, 0.032906335, -0.029479047, 0.11580199, -0.05812372, -0.032269973, 0.05219915, 0.041658226, 0.010897959, 0.065550454, 0.0076911976, -0.045743827, 0.11614996, -0.10393113, -0.0012606392, -0.034367524) * go_3(-1.0, 1.0); result += mat4(0.09350742, 0.09561609, 0.3735968, 0.031685118, -0.042026598, 0.17006761, -0.3910107, 0.16984761, 0.25679177, 0.036610503, -0.13772772, 0.11101589, -0.1137049, 0.07211461, 0.18065079, -0.12324793) * go_3(0.0, -1.0); result += mat4(-0.020749722, 0.14413361, -0.061903823, -0.21550268, 0.31306142, -0.11532895, 0.029482557, 0.03282164, -0.09800627, -0.20765196, 0.33030233, 0.075725295, 0.49252015, 0.042455837, -0.07264194, -0.10401895) * go_3(0.0, 0.0); result += mat4(-0.22697076, -0.15738785, 0.09740376, -0.072098814, -0.06638972, 0.12336611, 0.0073687397, 0.048267826, 0.06717852, -0.027047804, -0.123397194, 0.17829034, 0.04215185, 0.066311836, -0.061742183, -0.046373066) * go_3(0.0, 1.0); result += mat4(0.041311592, 0.2813485, 0.055084586, -0.01823069, 0.08105147, -0.087944716, -0.10135052, -0.02653456, 0.063169874, -0.1351186, 0.06722432, -0.016406318, 0.08666922, 0.0555909, 0.12086502, -0.17224412) * go_3(1.0, -1.0); result += mat4(0.26026788, -0.18303715, 0.029279215, -0.12858874, 0.027197823, 0.0919464, 0.00849638, 0.10547888, -0.12952055, -0.14414985, 0.1903315, 0.05004528, -0.12657289, 0.038008716, -0.036606666, -0.054025438) * go_3(1.0, 0.0); result += mat4(0.069167465, 0.2699947, -0.11137602, -0.05888806, -0.107324794, -0.07598601, 0.06042177, 0.0064530694, -0.039780665, -0.076666445, -0.00846108, -0.06165907, -0.06978219, -0.19108103, -0.040026028, -0.120319635) * go_3(1.0, 1.0); result += vec4(-0.14375664, -0.0056876075, 0.052177623, 0.07152566); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(-0.15667982, -0.31441393, 0.29112124, -0.15737213, 0.022372838, 0.10690639, -0.12019085, -0.051941186, -0.30367845, 0.02612279, 0.2372532, 0.2021648, -0.20481086, -0.003770439, 0.14981231, 0.066780254) * go_0(-1.0, -1.0); result += mat4(0.03270688, -0.42270073, 0.044317324, 0.15907793, 0.14681059, -0.2934784, 0.24933252, -0.067273855, 0.07752533, -0.23194817, 0.0686707, 0.08999225, 0.121678345, -0.12916678, 0.012397381, 0.012315053) * go_0(-1.0, 0.0); result += mat4(-0.10090412, -0.20792678, 0.11076032, -0.02938975, -0.1944187, -0.2003259, 0.04438032, 0.36946484, -0.019868722, -0.15830222, 0.042811528, 0.015641417, 0.113098525, 0.080257006, 0.011135628, -0.2877629) * go_0(-1.0, 1.0); result += mat4(0.15482685, 0.06579119, 0.28301102, 0.23729764, 0.15990537, 0.4529694, 0.107880585, 0.10668121, -0.42430598, -0.2631025, 0.10513542, -0.036242936, -0.09827965, -0.0069260495, -0.11689201, -0.041436482) * go_0(0.0, -1.0); result += mat4(0.08472191, -0.13051608, 0.047930017, 0.36831668, 0.1164478, 0.21384816, 0.22062506, 0.2094167, 0.48668453, 0.32302913, 0.36268055, -0.091801375, -0.079141125, -0.26613805, -0.16608004, 0.03810683) * go_0(0.0, 0.0); result += mat4(-0.13474251, -0.04824603, 0.23303726, -0.116136365, 0.0056330245, 0.15829784, 0.0012259148, 0.12648389, 0.038680512, 0.05131116, 0.024099711, 0.4555406, 0.0035716395, 0.11633299, 0.094744846, -0.2457627) * go_0(0.0, 1.0); result += mat4(-0.0576871, -0.04037522, 0.16857862, 0.0031084458, -0.027274646, -0.18154246, 0.13337846, 0.035422433, -0.0030749738, -0.17288287, 0.019983152, -0.31871706, -0.03280405, 0.06825421, -0.1563798, 0.05031885) * go_0(1.0, -1.0); result += mat4(-0.066631876, 0.012560506, 0.1690693, -0.018248236, 0.0450104, 0.016296914, -0.14910112, -0.16191053, 0.5078224, -0.017615631, 0.15226597, -0.13373777, 0.20148668, 0.060258996, 0.13215344, 0.18430072) * go_0(1.0, 0.0); result += mat4(0.12976126, -0.072738245, 0.053067926, 0.09752956, -0.04716214, 0.04136464, 0.014162617, -0.06621296, -0.09617736, 0.057469178, 0.01280261, -0.042976785, -0.12570308, 0.006027807, 0.031038594, 0.06569918) * go_0(1.0, 1.0); result += mat4(-0.12655424, -0.41563693, -0.030971345, -0.06357555, -0.14121394, -0.15667427, 0.14398985, 0.05995984, 0.0821605, 0.12462943, 0.007492498, -0.0030187522, -0.22804567, -0.10487421, 0.13180672, -0.13978589) * go_1(-1.0, -1.0); result += mat4(-0.075991526, 0.12352044, -0.17844258, 0.010614991, -0.18293494, 0.25009897, -0.080779895, 0.21548378, 0.22215544, 0.048670914, -0.057372037, 0.078176, 0.17490411, 0.004919551, 0.059619516, 0.12660357) * go_1(-1.0, 0.0); result += mat4(-0.06282951, 0.10929357, 0.026720649, -0.15939257, 0.17107709, -0.04334904, -0.03047162, -0.101681694, 0.03118431, 0.19994627, 0.025729552, 0.035035726, -0.0012207883, -0.08618888, 0.061205562, 0.009940555) * go_1(-1.0, 1.0); result += mat4(-0.23581573, 0.08002133, -0.15170844, 0.08872338, -0.25767094, -0.09273545, 0.18153891, 0.2544269, -0.084598936, -0.089766875, -0.14610913, 0.002247754, 0.1802837, -0.019625561, 0.30239686, -0.032793984) * go_1(0.0, -1.0); result += mat4(0.5223286, 0.10347663, 0.4000593, 0.25440502, -0.07646958, -0.31940606, 0.053407036, -0.09356492, 0.2738851, 0.23945184, -0.2907089, -0.45822915, 0.13415676, 0.17187089, 0.08731114, -0.27670014) * go_1(0.0, 0.0); result += mat4(0.059273496, -0.107137166, 0.12087539, 0.179237, -0.021209063, -0.02548005, 0.061256204, 0.033822674, 0.54491127, -0.2475085, 0.08055858, -0.4071213, -0.045093834, 0.07161349, 0.08219979, -0.31735933) * go_1(0.0, 1.0); result += mat4(-0.29527053, 0.021469543, 0.07202354, -0.07103959, 0.03990857, 0.2490762, -0.19419849, -0.13916986, -0.05325315, 0.12922864, -0.041463424, -0.031249814, 0.073991664, -0.09723187, 0.35132217, 0.024760868) * go_1(1.0, -1.0); result += mat4(0.09606787, -0.0951808, -0.0059865676, -0.052033573, -0.3118038, 0.4432636, -0.12943317, 0.09484738, 0.10621756, -0.10550469, 0.11264014, 0.1402276, -0.012679125, -0.08809835, 0.029994955, -0.15121669) * go_1(1.0, 0.0); result += mat4(0.123397775, 0.048338536, -0.00975707, -0.103767075, -0.041053303, -0.07228534, 0.046792876, 0.0668788, 0.29554394, 0.012451002, 0.19568972, 0.112091154, 0.10882395, -0.0995439, 0.051324263, 0.24967718) * go_1(1.0, 1.0); result += mat4(0.2699648, 0.17300771, -0.16056584, 0.1099392, 0.11674778, -0.19811755, 0.111880325, -0.06075038, -0.095849104, -0.04510651, -0.04180761, -0.0052786698, 0.11037549, -0.24115366, 0.018509468, -0.07819484) * go_2(-1.0, -1.0); result += mat4(0.10981622, 0.044488225, 0.050722387, -0.3146652, -0.0013019707, -0.24084032, -0.10475088, 0.026944289, 0.1592903, 0.33087498, 0.061839584, -0.043863457, -0.06904603, -0.08635262, 0.088630445, -0.15485142) * go_2(-1.0, 0.0); result += mat4(-0.06810522, 0.19927117, -0.08130387, 0.11612667, -0.015104349, -7.738651e-05, -0.06419643, -0.14813533, 0.026650215, 0.015038833, 0.08161237, 0.058321163, 0.015005185, -0.16189656, 0.024501886, 0.1927279) * go_2(-1.0, 1.0); result += mat4(0.31858218, 0.11962043, -0.20560326, -0.13190113, 0.02138715, -0.057066392, -0.085771754, -0.124566585, 0.044749223, 0.13687828, 0.1195792, 0.14021616, 0.26204133, 0.05119197, -0.13980037, 0.050747477) * go_2(0.0, -1.0); result += mat4(-0.21238558, -0.0734057, -0.2036023, -0.34308743, -0.29370925, 0.2393742, -0.37877437, 0.036869828, -0.17053255, -0.26900926, -0.23330869, 0.32902205, -0.4882585, 0.27430108, -0.033711653, 0.15501487) * go_2(0.0, 0.0); result += mat4(0.23487025, 0.085289046, -0.14281847, 0.12543266, 0.15871634, -0.13858907, 0.14810285, -0.0239261, 0.1286852, 0.07754033, 0.01072327, -0.14313328, 0.05480442, -0.12195059, 0.11341822, 0.08224607) * go_2(0.0, 1.0); result += mat4(0.19490337, 0.023521842, -0.24548791, 0.0035114093, -0.07937166, -0.07674376, 0.08365873, -0.003286068, 0.023862893, 0.009626835, 0.032829892, 0.0078141205, 0.053484406, -0.08297165, 0.09303188, 0.004273738) * go_2(1.0, -1.0); result += mat4(-0.0032906602, 0.13636959, 0.027821168, 0.06270053, 0.024775786, -0.077529594, 0.03799126, 0.030000908, 0.031749167, 0.04360487, 0.004448846, -0.17835903, -0.30834544, 0.013150946, -0.13758293, -0.03296242) * go_2(1.0, 0.0); result += mat4(-0.14166978, 0.034131095, 0.049779188, 0.09453289, -0.011406557, -0.07020709, -0.0031981543, -0.03443845, -0.00010218944, 0.0855161, -0.10951453, 0.042758763, 0.1718446, -0.1577923, 0.0410027, -0.04992991) * go_2(1.0, 1.0); result += mat4(0.1219178, 0.105126485, -0.041097324, -0.08110963, -0.04857337, -0.11544925, -0.14572923, 0.092435546, 0.091857366, 0.15425235, -0.020324683, -0.05764375, -0.020458939, -0.10527823, -0.085554086, 0.16358297) * go_3(-1.0, -1.0); result += mat4(-0.12372687, -0.009976829, 0.14252265, -0.1321053, -0.05965866, -0.1393898, -0.017603246, -0.02714342, -0.16824952, -0.23083204, -0.012299022, -0.06689838, -0.015830487, 0.21299921, -0.11637202, 0.0074968333) * go_3(-1.0, 0.0); result += mat4(-0.01979935, -0.182785, -0.015397454, 0.14175794, -0.011465284, 0.11285164, -0.036115747, 0.07150463, -0.083641894, -0.10221778, -0.13871445, 0.099696055, 0.04603662, -0.06463785, -0.007984529, -0.0032940735) * go_3(-1.0, 1.0); result += mat4(0.072830334, -0.057334073, 0.09086239, 0.13039105, 0.06350303, 0.17130788, -0.2181585, -0.09137403, -0.31397742, -0.019071499, -0.017274613, 0.13762084, 0.10195637, -0.021455176, 0.04011394, -0.08029658) * go_3(0.0, -1.0); result += mat4(-0.26982597, -0.40265098, -0.4151411, 0.038557775, -0.095602125, 0.3503172, -0.029988842, -0.03484708, 0.095536314, -0.0030311556, 0.31589827, 0.52763534, -0.12629713, -0.24356791, 0.0059487303, 0.42298427) * go_3(0.0, 0.0); result += mat4(0.054166105, 0.18827972, -0.081673265, -0.06720384, 0.09375001, 0.22173035, -0.14050071, 0.108400136, -0.15553835, -0.08716729, -0.037366748, 0.10971073, -0.02560103, -0.26702073, -0.05201882, 0.2432563) * go_3(0.0, 1.0); result += mat4(0.16196893, 0.0889265, -0.09887943, -0.042956755, -0.054403376, -0.123823255, 0.045847844, 0.017027669, 0.00539936, -0.112265736, 0.050549984, -0.104931094, -0.06883012, -0.25745714, 0.11155538, -0.15363649) * go_3(1.0, -1.0); result += mat4(-0.22157209, 0.18200903, -0.13290548, 0.026721261, -0.06066069, -0.18150693, 0.08768983, 0.037362453, -0.1073367, -0.070236765, -0.41223463, -0.168915, -0.15517351, -0.13949952, -0.13307643, -0.15935421) * go_3(1.0, 0.0); result += mat4(-0.026589906, 0.0930502, 0.05195435, 0.06301585, -0.01107014, -0.019382332, 0.027223695, -0.004045145, -0.15238355, -0.0345132, 0.06355168, 0.0011230056, 0.16690113, 0.0017829507, -0.0023939044, -0.09471834) * go_3(1.0, 1.0); result += vec4(0.024455175, 0.01669877, -0.066231176, 0.036848705); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf1 //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.01763509, -0.17156707, -0.06841296, -0.026132878, -0.10600523, 0.11245994, 0.121395074, -0.09331501, 0.12764473, 0.0428028, -0.11837395, 0.2092563, -0.04357652, -0.0490096, 0.024701532, 0.10518723) * go_0(-1.0, -1.0); result += mat4(-0.17130826, -0.31987694, -0.07639005, 0.21362033, 0.058639023, 0.066175915, -0.25344703, -0.07923442, -0.14766373, 0.040518284, -0.031103026, -0.040075514, -0.051108997, -0.28214613, -0.18504949, 0.27544948) * go_0(-1.0, 0.0); result += mat4(0.030991005, -0.011353306, 0.15237464, 0.15458584, 0.1250524, 0.19959912, 0.14049476, 0.38410887, 0.07378578, -0.017728366, 0.0963528, -0.043756213, -0.039577194, -0.11800575, -0.08392266, -0.07599512) * go_0(-1.0, 1.0); result += mat4(0.022089608, -0.027317125, 0.051330008, -0.0075439885, 0.021650828, -0.0009390209, -0.12043464, 0.049332134, -0.055557396, -0.053297505, -0.0918705, -0.13089466, -0.10994107, 0.072746456, 0.11496739, -0.05225977) * go_0(0.0, -1.0); result += mat4(0.29730305, 0.26317745, 0.052159555, -0.32006654, 0.48288685, -0.049926184, -0.08091092, -0.13825637, -0.1485706, -0.288657, -0.41443697, 0.06856032, -0.23809211, -0.12953928, 0.4783034, -0.47557938) * go_0(0.0, 0.0); result += mat4(0.026139118, -0.23031352, 0.04861487, 0.033556074, 0.2702056, 0.22802536, -0.15385233, 0.1664119, 0.18749923, 0.36927548, -0.011473684, -0.11771165, -0.16859052, -0.4513202, 0.12863952, 0.02482837) * go_0(0.0, 1.0); result += mat4(0.0073229345, -0.061915245, 0.06710329, 0.0062416573, -0.00555983, 0.14592186, 0.11201052, -0.123630054, 0.32611257, -0.11279885, -0.059449438, 0.2891043, -0.10519016, 0.040108994, -0.012468261, 0.02083298) * go_0(1.0, -1.0); result += mat4(-0.057483062, 0.08454755, -0.15529329, -0.12572923, 0.2600099, -0.02319978, -0.04037675, 0.11496361, 0.07728194, -0.12908956, -0.025529336, 0.112581626, 0.02971823, 0.11659056, -0.01298622, 0.017061908) * go_0(1.0, 0.0); result += mat4(0.22417091, -0.00222947, 0.04980858, 0.12260437, -0.025507605, 0.042577885, 0.120813504, -0.048522256, -0.038494784, -0.0072195013, -0.23012944, -0.020850847, -0.078296244, -0.014830018, 0.19759563, -0.10000253) * go_0(1.0, 1.0); result += mat4(-0.032090195, 0.023757193, -0.08989734, 0.14419042, 0.0112194475, -0.093776144, -0.020197887, 0.29295877, 0.06872183, 0.09511462, -0.03245769, -0.06504889, 0.05132126, 0.00399527, 0.075911656, 0.250893) * go_1(-1.0, -1.0); result += mat4(-0.3418496, 0.25525784, 0.0018161442, 0.028484365, -0.17573346, -0.12457501, 0.18466166, 0.20209278, 0.10282706, 0.16353399, 0.025052028, -0.059714165, -0.055806916, -0.28651386, 0.112798095, 0.11624314) * go_1(-1.0, 0.0); result += mat4(-0.018793896, 0.07500149, -0.01728254, -0.1726998, -0.13333, 0.09590344, -0.036537904, -0.11522523, 0.19445558, 0.22680458, 0.12061006, -0.06225618, 0.1127748, 0.28380096, -0.07099846, -0.007440302) * go_1(-1.0, 1.0); result += mat4(-0.43887648, -0.10018577, -0.29267642, 0.12149727, -0.14333835, 0.04161915, 0.19442867, 0.16506511, 0.09655387, -0.0014398015, 0.13189743, -0.14068556, 0.049408, 0.0829072, 0.2950336, 0.36965907) * go_1(0.0, -1.0); result += mat4(0.41486958, -0.023498302, -0.37900022, -0.31752598, 0.13758768, -0.18782206, -0.31358528, 0.3330786, -0.4039293, -0.06539036, 0.032599606, 0.10663507, -0.26369813, -0.17365438, 0.20723309, 0.1801556) * go_1(0.0, 0.0); result += mat4(0.004117444, -0.14894462, 0.14915143, -0.047375835, -0.2609916, -0.10172324, -0.14925237, -0.33830285, 0.12131607, -0.18156646, -0.42382464, -0.052582145, 0.2329045, -0.4576963, 0.13756892, 0.055571318) * go_1(0.0, 1.0); result += mat4(-0.31689477, 0.017058033, -0.01904924, -0.016893756, -0.011479519, 0.07316262, -0.07086077, 0.08923511, -0.08190091, -0.025866933, -0.06909204, -0.028601022, 0.023224542, 0.03082087, 0.2230426, -0.16713654) * go_1(1.0, -1.0); result += mat4(0.13457374, 0.110913865, -0.1130815, -0.031438913, -0.55201167, 0.04831016, 0.25107765, -0.014003224, 0.19532952, 0.02062346, 0.04839241, 0.088673405, 0.30325848, -0.20222804, -0.085780576, 0.22512968) * go_1(1.0, 0.0); result += mat4(0.076354, 0.021940092, -0.16170324, 0.0025543426, -0.0032400405, -0.0046705627, 0.06241069, -0.031247333, 0.098353796, 0.03723474, 0.22971998, -0.017877292, 0.119858086, 0.008041448, 0.2140585, 0.10343376) * go_1(1.0, 1.0); result += mat4(0.08627595, 0.04532834, 0.027579082, -0.16222088, 0.15583228, -0.14371829, -0.07243855, -0.111895435, -0.14438897, -0.10250594, 0.0034202964, -0.066547595, -0.034390844, -0.021545287, 0.014540157, -0.10215731) * go_2(-1.0, -1.0); result += mat4(0.19720152, 0.21534947, 0.1130938, -0.011730973, 0.013247983, -0.10344174, -0.1906514, -0.015767017, -0.020093633, -0.26487067, -0.005960781, -0.057149183, 0.030110173, 0.047692046, -0.19308545, -0.25292158) * go_2(-1.0, 0.0); result += mat4(0.039498243, 0.053682897, -0.01844695, -0.017540915, 0.039454967, -0.27696076, 0.09503274, -0.038958035, 0.17321438, -0.036311295, 0.03123055, 0.02310311, 0.040591653, 0.0054627894, -0.03520426, -0.026101988) * go_2(-1.0, 1.0); result += mat4(0.055991564, 0.06512919, -0.12532505, 0.024075158, -0.04926237, -0.11701171, 0.026792146, 0.013033238, -0.052847516, -0.01550091, -0.008442071, -0.077945165, -0.033220004, -0.13678443, -0.07040586, 0.121846326) * go_2(0.0, -1.0); result += mat4(-0.19537796, -0.016634773, 0.10707109, -0.024361614, -0.16002733, -0.44066608, 0.16488662, 0.013152995, 0.22407806, 0.12854017, 0.19028598, -0.08379244, -0.05594235, -0.15909895, 0.511962, 0.39027596) * go_2(0.0, 0.0); result += mat4(-0.032652248, 0.06004893, 0.011166194, 0.102761306, -0.035113614, -0.29961765, -0.013817978, 0.20938557, 0.08488225, -0.1118558, -0.0375328, -0.035511103, 0.0046933405, 0.20203683, -0.13552529, -0.12685429) * go_2(0.0, 1.0); result += mat4(0.03054923, 0.08224908, -0.059128158, -0.02583655, -0.02133876, 0.0048713544, 0.10848829, 0.06324404, 0.028332822, -0.011002306, -0.027557913, -0.06072362, 0.1019048, -0.02587316, 0.08563405, -0.08119947) * go_2(1.0, -1.0); result += mat4(-0.10568117, 0.1075248, 0.19379964, -0.14337265, 0.019374132, -0.0907804, -0.13827625, -0.03628561, 0.014735499, -0.026882607, -0.25948793, 0.034926686, -0.05988073, -0.22735636, 0.053511668, 0.04765336) * go_2(1.0, 0.0); result += mat4(-0.029848114, 0.09183966, 0.084713496, 0.09422864, 0.069713995, -0.10584984, -0.020899031, 0.059645247, -0.075805016, -0.01828552, 0.06689195, -0.13804196, -0.023465823, -0.034038994, -0.12946706, 0.058709413) * go_2(1.0, 1.0); result += mat4(0.061918218, 0.038984764, 0.013660938, -0.19340219, -0.014949839, 0.12946278, 0.12725051, 0.13429146, 0.05993008, -0.015394284, 0.011232483, 0.0344157, 0.022161875, -0.023923954, 0.061736204, 0.025963215) * go_3(-1.0, -1.0); result += mat4(0.048136763, 0.03162042, -0.01967249, 0.06374493, 0.034645267, 0.22403605, 0.036197048, -0.06903216, -0.1024706, -0.0005459356, 0.049185563, 0.16309108, 0.07394778, 0.10351343, 0.28430694, -0.13531347) * go_3(-1.0, 0.0); result += mat4(-0.14705071, -0.09458433, 0.03063114, 0.07901115, -0.11911086, -0.06428132, -0.013549552, -0.041342866, -0.20770676, -0.15104479, 0.054365363, -0.11652907, 0.05639815, 0.070518605, 0.0017846811, -0.00056205114) * go_3(-1.0, 1.0); result += mat4(0.27148908, 0.07358356, 0.13644488, -0.13824654, 0.0112991175, -0.021521023, -0.10197379, 0.007816017, -0.13314332, 0.12318473, -0.043214846, -0.15759036, -0.19744353, -0.10267182, -0.28249928, 0.11233295) * go_3(0.0, -1.0); result += mat4(-0.096474804, 0.17893109, 0.014679829, -0.21218887, -0.24170275, 0.10603527, 0.05375366, -0.059315052, 0.17087384, 0.13633691, -0.37958893, 0.43264794, 0.17829923, 0.06485103, -0.37551817, -0.22082718) * go_3(0.0, 0.0); result += mat4(-0.30536333, -0.033212308, -0.25232, 0.11730442, -0.11176368, 0.26223183, -0.049025323, -0.01375941, -0.29028055, 0.16842811, -0.035684332, -0.4180911, -0.1611732, 0.07683385, -0.14263596, 0.17508087) * go_3(0.0, 1.0); result += mat4(0.23580009, 0.025621435, -0.15757325, 0.008123166, -0.021905439, -0.02162503, -0.059497356, -0.01636353, 0.047654126, -0.084423855, -0.033733923, 0.0127116265, -0.059593942, -0.053935718, -0.050729543, 0.013887048) * go_3(1.0, -1.0); result += mat4(-0.19232626, 0.07915767, -0.05909752, 0.007695347, 0.058876406, 0.057521783, -0.080253534, 0.2011056, -0.27965516, -0.08033169, -0.13025513, 0.12854645, 0.053400308, -0.18445957, -0.18463044, 0.27920377) * go_3(1.0, 0.0); result += mat4(-0.061806213, -0.020037206, 0.003183183, -0.029844081, -0.039553937, 0.028905323, -0.11367984, -0.097321615, -0.10112643, 0.0039709485, -0.06020118, -0.23871279, -0.077974856, 0.05806996, -0.21440302, 0.11898043) * go_3(1.0, 1.0); result += vec4(-0.023832673, 0.03702965, -0.04749135, -0.10982549); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.030931145, 0.013683292, -0.0650242, -0.028732346, 0.120067924, -0.029404473, 0.0038229884, -0.14631765, 0.041900825, -0.076596744, -0.11096378, -0.27100095, 0.0052598766, -0.05929686, -0.06816563, -0.086864315) * go_0(-1.0, -1.0); result += mat4(-0.043620087, -0.16360405, 0.006527374, 0.15706524, 0.08338088, -0.19027525, 0.22595987, -0.054963548, 0.01825031, -0.03149212, 0.025471251, 0.06429379, -0.011633275, -0.079389006, -0.0030728737, 0.17345747) * go_0(-1.0, 0.0); result += mat4(-0.011275288, -0.10668036, 0.05718997, 0.010336089, 0.33393976, -0.2029354, 0.075444475, -0.092244044, 0.07605498, 0.20125951, 0.10493973, -0.12306946, 0.03658231, 0.08233366, -0.12205888, -0.116969004) * go_0(-1.0, 1.0); result += mat4(-0.0070305974, 0.105127215, 0.006041873, 0.26743913, 0.028119443, 0.14823505, -0.28344348, 0.12362866, -0.1215781, 0.08104382, 0.102011785, 0.085380934, 0.061244503, -0.06230063, -0.05353345, 0.1166729) * go_0(0.0, -1.0); result += mat4(0.08945733, 0.4101902, -0.06404005, 0.040728435, 0.13076581, -0.20805469, -0.10897316, -0.14924604, 0.10090762, 0.015475414, 0.26346552, 0.12096677, -0.20199244, 0.2780031, 0.18515368, 0.35105625) * go_0(0.0, 0.0); result += mat4(0.07463155, 0.26932517, -0.06768551, 0.10470878, -0.1423996, 0.013550665, -0.06167201, -0.1022994, -0.3107166, -0.15609552, 0.1695213, -0.1277181, 0.12582655, -0.1596128, 0.015612055, -0.19826376) * go_0(0.0, 1.0); result += mat4(0.011745468, 0.006471601, 0.008110513, 0.025831396, 0.1272883, -0.221959, 0.11993834, -0.007903633, 0.009993582, -0.10170755, 0.026594637, -0.027883623, 0.030666083, -0.036415886, 0.007469573, 0.0674783) * go_0(1.0, -1.0); result += mat4(-0.022760388, -0.10911659, -0.012589904, -0.046462692, 0.36987287, 0.71668935, -0.04466556, 0.12082762, 0.0026539841, 0.07070946, -0.00020439121, -0.13925348, 0.08672072, 0.20075354, -0.066352285, 0.14655356) * go_0(1.0, 0.0); result += mat4(-0.081081845, -0.21956222, 0.06781787, -0.106362104, -0.03016425, -0.010460211, -0.009725996, -0.009805538, 0.07037355, 0.19254607, 0.038890257, 0.29580075, -0.10355764, 0.12613009, 0.02485986, -0.031927988) * go_0(1.0, 1.0); result += mat4(-0.13882205, 0.21770848, 0.015392157, 0.010310204, 0.008225721, 0.07457836, 0.09984027, -0.25452816, 0.2193511, -0.22262146, -0.12950355, 0.026151875, 0.022114651, -0.030566849, 0.034688126, 0.03047327) * go_1(-1.0, -1.0); result += mat4(0.0363441, 0.19290726, -0.1143055, 0.30871987, -0.05780708, 0.082128406, -0.115280904, 0.07636388, 0.48947453, -0.29715258, 0.146737, -0.3275992, -0.055972476, -0.09991753, 0.17435446, 0.10917291) * go_1(-1.0, 0.0); result += mat4(0.026389305, 0.054523308, -0.028950177, 0.06913328, -0.18626037, 0.08829993, 0.10407121, 0.001246911, 0.103938825, -0.3117343, -0.045564886, 0.07316613, 0.0027089121, 0.099437356, -0.046500806, -0.0927284) * go_1(-1.0, 1.0); result += mat4(0.051037624, -0.2068234, 0.061572235, -0.3345198, 0.16960172, -0.30289862, -0.002583443, 0.39312238, 0.08246557, 0.16374862, -0.31902805, -0.13205275, -0.032050006, 0.01670186, 0.13852347, 0.120012194) * go_1(0.0, -1.0); result += mat4(-0.67096996, -0.06274476, 0.18575665, 0.80282855, 0.23201196, -0.0054729837, 0.050396994, -0.42014772, 0.34904522, 0.26281372, 0.24697208, 0.55475426, 0.49850988, -0.06581312, -0.0068906257, -0.15741143) * go_1(0.0, 0.0); result += mat4(-0.04252036, -0.28224963, 0.009723064, 0.116357096, 0.2992567, -0.26702902, -0.05648925, 0.12729199, -0.37574205, 0.54211813, -0.25248805, -0.13023548, 0.18903324, -0.5182459, 0.0141203115, -0.19444294) * go_1(0.0, 1.0); result += mat4(-0.0017735233, -0.010132458, -0.040924776, -0.13767008, 0.20757031, -0.06509882, -0.09756446, 0.018974079, 0.090851985, -0.010158765, -0.03999607, -0.12055641, 0.03629025, -0.018645551, -0.05506811, -0.014202848) * go_1(1.0, -1.0); result += mat4(0.16203491, 0.011118734, -0.18486023, -0.024290733, -0.3673846, -0.20295864, 0.23055002, -0.1555852, -0.02706522, 0.03262891, 0.008724611, -0.03760652, -0.20946771, -0.01951837, 0.16955496, 0.11690098) * go_1(1.0, 0.0); result += mat4(0.0783421, 0.22656651, -0.15715368, -0.024174158, 0.020260733, 0.032390315, -0.029133298, 0.086601086, 0.13871798, -0.12525433, 0.16097449, 0.058946393, 0.029865682, 0.08508385, 0.040569812, -0.09402932) * go_1(1.0, 1.0); result += mat4(-0.05063873, 0.11269313, -0.057484943, -0.13579641, 0.047973365, -0.07103839, -0.07838756, -0.0028928046, -0.019466015, 0.018428024, 0.010016324, -0.057396665, -0.19495595, 0.034307264, -0.022888038, 0.08112259) * go_2(-1.0, -1.0); result += mat4(-0.09790086, 0.10613111, 0.06611674, 0.19356097, -0.00073371036, -0.019078335, 0.076719105, -0.016212497, -0.3283475, -0.07547389, -0.08140701, 0.3185625, -0.25060275, 0.16820994, -0.123497784, 0.43272668) * go_2(-1.0, 0.0); result += mat4(-0.06365342, 0.11186735, -0.17493224, -0.04207358, 0.0003117533, 0.034089327, -3.067692e-05, -0.03422754, 0.16267666, 0.054771993, 0.048384454, -0.041866794, 0.0036008756, 0.0021496525, 0.20258942, -0.06297619) * go_2(-1.0, 1.0); result += mat4(0.03578836, 0.08763908, -0.22370125, -0.32465744, 0.019142643, 0.011316954, 0.17920344, 0.031633645, 0.03766343, -0.116487674, -0.05281752, -0.018965483, 0.049297336, -0.34511214, 0.42598158, 0.051361635) * go_2(0.0, -1.0); result += mat4(0.26638633, -0.33628765, 0.04437907, 0.09616201, -0.020049393, 0.2560829, -0.027108455, 0.255752, 0.3666511, 0.052277412, -0.46667686, 0.48482272, 0.51302284, -0.06941614, -0.17967525, -0.07889891) * go_2(0.0, 0.0); result += mat4(0.18503937, 0.088710256, 0.2083147, -0.20758459, -0.036416974, 0.018303726, 0.03729963, -0.035969947, -0.2685231, -0.42169708, -0.039593916, -0.02642618, 0.29050872, -0.25723743, -0.111259766, 0.15001127) * go_2(0.0, 1.0); result += mat4(-0.026473878, -0.07241443, 0.022400148, -0.03214132, 0.0859297, -0.0036677981, -0.07039137, 0.03703108, 0.042322673, -0.01222808, -0.08151938, 0.033109214, -0.048737407, 0.25929528, -0.40535828, -0.123594694) * go_2(1.0, -1.0); result += mat4(0.10233285, 0.22455986, -0.13368733, 0.033236265, -0.052114893, -0.11709317, 0.009709581, 0.19201641, -0.02973698, 0.032114245, -0.09771862, 0.085680574, 0.15827927, -0.15042172, 0.21833214, -0.13262676) * go_2(1.0, 0.0); result += mat4(-0.08460587, -0.09473209, 0.019323658, -0.057233352, 0.0019434267, -0.14437936, 0.034232683, 0.0030602294, -0.023598112, 0.10692026, -0.09960999, 0.005887181, 0.014738836, -0.32473162, -0.10886747, -0.08365826) * go_2(1.0, 1.0); result += mat4(0.10900178, 0.00080280803, -0.14009437, -0.053074867, -0.07811151, -0.03456029, -0.104943685, 0.016918905, -0.11335709, 0.079421654, 0.13481963, 0.037818357, -0.027339859, 0.05856774, -0.044562265, 0.03908084) * go_3(-1.0, -1.0); result += mat4(0.07628258, -0.23815769, 0.2840278, -0.3541637, -0.044292126, -0.09310441, -0.1335055, -0.031899665, -0.11981227, 0.24012394, -0.041896038, -0.10168982, 0.20248915, -0.10036763, -0.044115108, 0.08520525) * go_3(-1.0, 0.0); result += mat4(0.07234102, -0.119480744, -0.01401321, -0.025182616, -0.031284854, -0.050089385, 0.014808948, 0.038662236, -0.18539418, 0.017342187, 0.023812262, 0.13428104, 0.020824855, -0.07433546, 0.054307282, 0.08511016) * go_3(-1.0, 1.0); result += mat4(-0.11046813, -0.04663274, 0.33497185, 0.023273284, -0.24681108, 0.116665915, 0.12045893, 0.13306482, -0.039098527, 0.04747061, 0.042796664, 0.053514794, 0.011861975, -0.048702, 0.008408589, -0.09497112) * go_3(0.0, -1.0); result += mat4(0.34634927, 0.37973458, -0.79267627, -0.7362719, 0.35489878, -0.07635863, 0.24082923, -0.27480397, -0.3236968, -0.25523046, 0.05118527, -0.040529836, -0.6000509, 0.39020586, 0.27632973, 0.5141453) * go_3(0.0, 0.0); result += mat4(0.16761221, -0.033125393, 0.00561569, 0.083019435, -0.101278506, 0.07810264, 0.12060661, 0.16048536, 0.14257826, -0.15996903, 0.018831912, -0.094429865, -0.22227801, 0.426937, -0.054677445, 0.05067348) * go_3(0.0, 1.0); result += mat4(0.02233958, 0.02608942, -0.045318656, 0.06509929, 0.035911568, 0.025316885, 0.0840986, 0.08326237, 0.048455603, -0.13630742, 0.07230253, -0.047261715, -0.092630014, 0.04786565, 0.10354939, -0.07094341) * go_3(1.0, -1.0); result += mat4(-0.1463382, -0.14900577, 0.2835977, -0.106733374, -0.11554754, -0.168429, -0.1411373, -0.20654152, -0.06388508, 0.039648015, 0.08543832, -0.13253337, 0.017264463, -0.06346233, -0.10823598, 0.067361064) * go_3(1.0, 0.0); result += mat4(0.04419582, 0.039152585, 0.06222691, 0.05757103, 0.012084537, 0.051425997, -0.061130576, 0.16752882, 0.07497411, 0.13495837, -0.15585983, -0.02050144, -0.08555421, -0.09147339, 0.025115604, 0.05948922) * go_3(1.0, 1.0); result += vec4(0.00590038, 0.03082865, 0.002111702, -0.03330112); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf1 //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { vec4 result = mat4(0.009029573, 0.029218858, 0.029705316, -0.019268971, -0.0023235187, -0.072589695, 0.1424836, 0.09049359, 0.04342995, 0.18134294, 0.018145641, 0.14789368, 0.050923645, 0.06524081, 0.036812488, 0.11108108) * go_0(-1.0, -1.0); result += mat4(-0.026506428, 0.016968496, 0.015961196, 0.010030791, -0.3141888, -0.06769598, -0.23920257, -0.031002127, -0.07351358, -0.19290134, -0.24282931, -0.18831016, -0.0928966, 0.075177215, -0.19699521, -0.05810917) * go_0(-1.0, 0.0); result += mat4(-0.017991852, -0.079427645, 0.035970494, -0.017095685, -0.27197137, -0.20046075, 0.2616644, 0.021876303, -0.077394076, -0.04978692, 0.20363241, -0.013741705, -0.032103598, 0.14403099, 0.01442474, 0.048115995) * go_0(-1.0, 1.0); result += mat4(-0.16939245, -0.001777, 0.026244136, -0.14122388, -0.056853324, 0.54357284, -0.19769607, -0.03187079, 0.04559263, -0.16048127, 0.12830622, 0.1442168, 0.006611398, -0.01618195, 0.012860053, -0.16539487) * go_0(0.0, -1.0); result += mat4(0.13116026, -0.006161343, 0.7209969, 0.18338475, 0.3099777, 0.6500026, 0.3883795, -0.021434233, 0.31667513, 0.008917659, 0.14124091, -0.22335114, 0.12198921, -0.16449445, 0.08773425, 0.30054978) * go_0(0.0, 0.0); result += mat4(-0.10413989, -0.10316161, 0.04342709, -0.021252686, 0.120892406, 0.37798002, -0.35963747, 0.021069285, 0.37587845, -0.08159587, 0.011139747, 0.2501104, -0.094568014, 0.037900843, -0.025109999, -0.030106556) * go_0(0.0, 1.0); result += mat4(0.09680291, -0.040868275, 0.051731605, 0.089064725, -0.56098557, -0.38148618, -0.017037416, 0.08508287, -0.019247344, 0.019857002, -0.03512887, 0.031057188, -0.09648583, -0.04474188, 0.028748507, -0.11880965) * go_0(1.0, -1.0); result += mat4(-0.010236943, 0.04257042, -0.08202597, -0.004203426, -0.26801527, -0.11716526, -0.017402772, -0.05819106, -0.13394608, 0.0234606, -0.15404865, -0.06801164, -0.0047627664, -0.1975249, 0.09420144, 0.23249897) * go_0(1.0, 0.0); result += mat4(0.107361935, 0.07373787, 0.06242962, 0.05236332, -0.028867323, 0.025924044, -0.042526353, -0.0015729597, -0.1323144, -0.4040712, 0.023919407, -0.09535502, 0.049100045, 0.081110805, 0.08946112, 0.058505684) * go_0(1.0, 1.0); result += mat4(0.13236825, -0.04468476, -0.04426802, 0.031087106, -0.09093992, -0.07470971, -0.01591504, 0.05924266, -0.21910913, 0.065537, -0.18358919, -0.02533145, -0.1512009, -0.04953928, 0.015540006, -0.0043442883) * go_1(-1.0, -1.0); result += mat4(-0.14016777, -0.1086958, 0.16316028, 0.050777458, 0.23148167, 0.04944809, -0.10599886, -0.10447021, -0.40729257, -0.10926556, 0.069055155, 0.110635415, 0.108922414, -0.1716362, 0.10743909, -0.102534756) * go_1(-1.0, 0.0); result += mat4(0.017795928, -0.066930935, 0.09396082, 0.092585504, 0.14223933, 0.059458215, 0.072033696, -0.04507726, -0.19956456, 0.1251282, -0.31733638, -0.10465904, 0.08546377, 0.048638333, 0.031372465, -0.08720661) * go_1(-1.0, 1.0); result += mat4(0.108719654, -0.092161916, -0.014724377, 0.20068261, -0.24350016, 0.2113636, -0.07483714, -0.45665312, -0.25134233, 0.2753893, -0.11324696, -0.04472, 0.1576102, -0.045395147, 0.06013951, -0.12507361) * go_1(0.0, -1.0); result += mat4(0.546225, -0.281897, 0.19477816, -0.116612464, -0.3145171, -0.41660902, 0.333625, 0.35902345, 0.48333502, 0.4662005, 0.10222491, -0.15314859, -0.3036888, 0.22849742, 0.20740797, 0.41399437) * go_1(0.0, 0.0); result += mat4(0.007284074, 0.0393942, -0.31192186, -0.15687793, -0.289214, -0.015956698, -0.24718472, -0.1637855, -0.00765037, 0.26677555, 0.20215511, 0.37790874, -0.22096673, 0.25287116, -0.2446764, -0.13610223) * go_1(0.0, 1.0); result += mat4(-0.16734968, 0.16721225, -0.053508647, -0.041097626, 0.062356673, 0.07812319, -0.263546, -0.39739034, 0.003389846, 0.12676363, -0.13175991, -0.19019242, -0.011847587, -0.007580052, -0.023946386, 0.046034034) * go_1(1.0, -1.0); result += mat4(-0.17047611, 0.13298693, -0.07506747, -0.045542978, 0.33571973, 0.20192616, 0.30674616, 0.25668672, -0.24134545, 0.031693842, -0.009647641, 0.040534843, 0.03159419, -0.1100516, 0.11371316, 0.06098735) * go_1(1.0, 0.0); result += mat4(-0.05518961, 0.19402988, -0.09646874, -0.059196774, -0.0073436056, -0.1381309, 0.06868669, 0.061328378, -0.1480867, -0.15774113, -0.022572191, 0.122521356, -0.04067007, -0.10145177, 0.13006335, -0.099452734) * go_1(1.0, 1.0); result += mat4(0.06962972, 0.07768411, 0.021085173, 0.108355984, -0.03132525, 0.10220273, -0.11626593, -0.14104277, 0.018778645, -0.024237925, 0.048783034, 0.09074447, 0.4120426, -0.01948466, 0.073218934, 0.055681944) * go_2(-1.0, -1.0); result += mat4(-0.22553118, -0.12923603, -0.22068842, -0.35037905, 0.005709937, -0.09528472, 0.08718399, 0.13200706, 0.17220478, 0.096844435, -0.30439013, -0.14122063, 0.15733318, -0.1014675, 0.33836862, 0.042193163) * go_2(-1.0, 0.0); result += mat4(0.15826897, -0.034870047, 0.09295099, -0.17674965, -0.042326324, 0.06680338, -0.074267656, -0.0631393, -0.11267909, -0.19795708, 0.22005288, 0.35703793, 0.033995766, -0.12663686, -0.02449896, -0.123250045) * go_2(-1.0, 1.0); result += mat4(0.021434195, 0.058398597, 0.04828315, -0.0016824572, -0.04291545, -0.0744907, -0.07698706, -0.15937585, -0.18852457, -0.17966963, 0.023800725, 0.025979731, -0.51412296, -0.018316887, -0.23076254, -0.12298674) * go_2(0.0, -1.0); result += mat4(0.16054317, -0.0002730893, -0.54173076, -0.62443435, 0.04300197, -0.08529622, 0.15392275, 0.15742144, 0.025834514, -0.2800517, -0.17600477, 0.0020806703, -0.3010582, 0.45233512, 0.25595665, 0.103661336) * go_2(0.0, 0.0); result += mat4(-0.024034392, -0.43800178, 0.28606912, -0.20908915, 0.078471914, -0.030501373, -0.059055753, 0.050494444, 0.063274644, -0.025071034, 0.17561312, -0.100698635, -0.25631955, 0.039981876, -0.18506624, 0.08366402) * go_2(0.0, 1.0); result += mat4(-0.1413656, 0.03589635, -0.020917566, 0.017598262, 0.020156413, -0.018854238, 0.027228508, -0.03806087, -0.021715842, 0.071974196, -0.040065665, 0.08459291, -0.23530225, 0.16599682, -0.2772327, 0.10041177) * go_2(1.0, -1.0); result += mat4(-0.055056706, 0.1286236, -0.11890451, -0.1790546, 0.16517544, -0.040448934, 0.12548013, 0.017075695, 0.07185459, -0.13236302, 0.19354409, 0.12767012, 0.31120765, 0.16378082, -0.036915366, -0.19724306) * go_2(1.0, 0.0); result += mat4(-0.02225051, 0.033263147, 0.003279449, 0.08826271, -0.047833472, 6.574577e-05, 0.13721916, 0.04801998, -0.014958419, 0.08791209, -0.08076282, 0.024002168, -0.18028922, 0.23835851, -0.23309888, -0.119310364) * go_2(1.0, 1.0); result += mat4(0.044960875, 0.18821983, 0.027640678, 0.013462449, 0.19011214, 0.21559924, -0.03329638, 0.07234414, 0.030880248, -0.11273214, 0.102028474, 0.12203351, 0.035855662, 0.008828778, 0.007218363, -0.012421797) * go_3(-1.0, -1.0); result += mat4(-0.09450626, 0.025191775, -0.10738468, 0.16237053, 0.073676676, 0.12488881, -0.048748355, 0.007877263, 0.3572506, -0.07911043, 0.14684045, 0.0015310893, -0.33411503, -0.1151223, 0.004201752, 0.017775744) * go_3(-1.0, 0.0); result += mat4(-0.10607509, -0.008143826, -0.08448629, -0.27557802, 0.0046665915, 0.008158659, 0.030826218, 0.020516023, 0.2333065, -0.017463414, -0.041772116, -0.03027809, -0.028166672, -0.080471426, 0.048199337, 0.08341059) * go_3(-1.0, 1.0); result += mat4(-0.14640257, -0.18334304, -0.061674733, 0.0008892598, -0.2374775, -0.2721524, -0.040371176, 0.26362613, 0.19872928, -0.11246391, 0.0842288, 0.11188515, 0.0045209546, -0.04250933, -0.0738212, -0.069005966) * go_3(0.0, -1.0); result += mat4(-0.08760266, 0.4816288, -0.21241407, 0.22734411, -0.1783721, -0.26842996, 0.099888, -0.2867675, 0.085521065, -0.3780281, -0.018543908, -0.039699722, 0.75688565, -0.5333645, 0.47567275, 0.09518891) * go_3(0.0, 0.0); result += mat4(-0.04072665, 0.05998423, -0.48314768, -0.29495844, 0.10358383, -0.09816629, 0.028586809, -0.047708735, 0.008320228, 0.04089551, -0.18359782, -0.27615002, 0.12414414, -0.072417594, 0.25932562, 0.30268723) * go_3(0.0, 1.0); result += mat4(0.14481631, 0.06484443, -0.09898657, -0.06553556, 0.25750044, -0.07265585, 0.12903488, -0.022347894, -0.04693863, -0.000107379274, 0.030295763, -0.0325354, 0.086214684, -0.021326948, 0.039682828, -0.034843277) * go_3(1.0, -1.0); result += mat4(-0.031971477, -0.25145087, 0.03931631, 0.14262606, -0.06044626, 0.22820354, -0.10506207, 0.18064679, 0.0069641788, 0.01477993, -0.003626875, 0.118767865, 0.109416224, -0.002998205, 0.035680585, 0.07843882) * go_3(1.0, 0.0); result += mat4(0.03375426, -0.059815384, 0.11632834, -0.12411481, 0.022583738, 0.02544465, -0.054889992, -0.07031964, -0.10140042, 0.16750422, -0.1448294, -0.09316004, 0.035582513, -0.026138382, -0.031955894, 0.040148776) * go_3(1.0, 1.0); result += vec4(-0.03573331, 0.032919675, 0.011109369, 0.008329268); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_last_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) vec4 hook() { vec4 result = mat4(-0.11498094, -0.053904895, -0.11520678, -0.05479549, 0.028396055, 0.032767884, 0.052479446, 0.05257866, -0.25706592, -0.3454966, -0.24713765, -0.2854201, -0.10287636, 0.0023146886, -0.09190338, -0.011193905) * g_0; result += mat4(-0.05461422, 0.008780496, -0.07738697, -0.032230727, -0.047554165, -0.025061952, -0.051897213, -0.009545297, -0.14548294, -0.15184018, -0.01313442, -0.015299784, -0.0007883845, -0.12866738, -0.15260352, -0.27081275) * g_1; result += mat4(0.11007706, 0.035344437, 0.11020841, 0.0425353, 0.1613199, 0.18417408, 0.09274313, 0.11943135, 0.106862, 0.079875536, 0.0937752, 0.068030775, 0.029093558, -0.06441164, 0.06467169, -0.021989612) * g_2; result += mat4(0.049548414, -0.012455486, 0.07185561, 0.021865537, 0.020969186, -0.03374196, -0.024260623, -0.07739141, 0.07164591, 0.12741035, 0.0379913, 0.076403245, 0.07049977, 0.0744538, 0.0062989634, 0.01818882) * g_3; result += mat4(-0.12511204, -0.010836819, 0.13709816, 0.22472954, 0.21280868, -0.006484726, 0.17554289, -0.009977173, 0.078398876, 0.20698707, 0.13432744, 0.29740283, -0.24750128, -0.32757792, -0.19807857, -0.2537023) * g_4; result += mat4(-0.27207088, -0.1385644, -0.2166476, -0.07687419, -0.20300622, -0.29678395, -0.13135734, -0.20851587, 0.0361364, 0.011243289, -0.06845459, -0.11796941, 0.11575868, 0.070215136, -0.10295678, -0.12281369) * g_5; result += mat4(0.13619795, -0.0019436983, -0.12701888, -0.25933513, -0.20134166, 0.00062823144, -0.076756015, 0.11002947, 0.0059049693, -0.18756741, -0.0718802, -0.2589954, 0.23413423, 0.30107784, 0.14445266, 0.18920745) * g_6; result += mat4(0.1494216, 0.0587532, 0.05478662, -0.039123338, 0.23322394, 0.29950607, 0.24384268, 0.27843767, -0.16094431, -0.04705998, -0.016345032, 0.028868208, -0.102872886, -0.04659664, 0.104105346, 0.14305067) * g_7; result += mat4(-0.001037014, 0.010001526, -0.0052278573, 0.024779709, 0.06857274, 0.067640975, 0.085439384, 0.09242789, -0.066597246, -0.055928994, 0.0015658981, 0.016131008, -0.03524695, -0.018364554, -0.047754433, -0.014295886) * g_8; result += mat4(-0.042207, 0.02835915, -0.1404656, -0.08563323, -0.030979915, -0.0673764, 0.10733943, 0.057902794, 0.00022424995, -0.0023634837, -0.10778953, -0.10202357, -0.020368295, -0.019088887, -0.06875738, -0.08504131) * g_9; result += mat4(-0.00043458896, 0.00045652856, -0.02016843, -0.020062413, -0.08740103, -0.042085808, -0.10644177, -0.09226477, 0.11212161, -0.00048174805, 0.021872435, -0.05868698, 0.0333954, 0.058184672, 0.05532576, 0.07621587) * g_10; result += mat4(0.054245148, 0.001020329, 0.09106849, 0.05303779, 0.009889632, 0.01309413, -0.09187347, -0.08618193, -0.011621187, 0.016222361, 0.061095525, 0.060885344, 0.078050986, 0.0111776795, 0.08829944, 0.032022282) * g_11; result += mat4(0.01643529, 0.02285545, -0.03498564, 0.00769657, -0.0042474116, 0.015836312, -0.025771018, -0.0016368, -0.008897948, -0.012588166, -0.01416411, -0.003578984, 0.025991246, 0.021237152, 0.017450012, 0.025172485) * g_12; result += mat4(0.014568868, 0.017796224, -0.036679734, -0.03138748, 0.019457601, -0.027607411, -0.004529679, -0.038048342, -0.054055385, -0.03876025, 0.041948095, 0.005869784, 0.02439633, 0.05177997, 0.016000897, 0.0057169925) * g_13; result += mat4(-0.03021866, 0.017678728, -0.01371109, 0.013548159, -0.0038099394, -0.014066414, 0.028093752, 0.0027308422, -0.010615999, 0.012673458, -0.03028171, -0.016818244, -0.06530097, -0.018845048, -0.0072947564, -0.0038243714) * g_14; result += mat4(-0.019006258, -0.007847591, 0.03690709, 0.06714211, 0.0073993434, -0.009766907, -0.0021441753, -0.01308625, 0.06658726, 0.06701995, -0.027305668, -0.016032105, -0.028976806, -0.0036668575, -0.0027825525, 0.0105632655) * g_15; result += mat4(0.028945107, -0.0014701135, 0.048950657, -0.01923516, -0.0014054152, 0.002650635, -0.005300331, 0.004860559, 0.011158468, 0.005940625, -0.012095051, 0.0041518128, -0.020433836, -0.025870577, -0.0007547932, -0.026509356) * g_16; result += mat4(-0.004545374, 0.04264545, 0.021741537, 0.029115127, 0.04225599, -0.0055392785, 0.026570829, -0.031795148, -0.008307126, 0.020176455, 0.010904648, 0.017765503, -0.10806103, -0.01776947, 0.00070428237, -0.06356262) * g_17; result += mat4(-0.05663172, 0.05908046, -0.03837452, 0.06636983, -0.007960516, -0.06384041, 0.023125881, -0.030108837, 0.0038054318, -0.023263922, 0.020264054, -0.0062937695, 0.031630237, 0.020909082, 0.03594235, 0.035879835) * g_18; result += mat4(-0.0050448794, 0.033650696, -0.002830413, 0.035174295, -0.024521282, 0.013054315, -0.020833842, 0.037953895, 0.08249671, 0.024239466, -0.012758333, -0.027316988, 0.051040914, 0.0005025873, 0.039778862, 0.0024668393) * g_19; result += mat4(0.017232442, 0.022482058, 0.020233413, 0.024337437, 0.07986929, 0.06234036, 0.12662584, -0.05271183, -0.009718745, -0.0046989853, -0.0030333172, -0.04034237, -0.0113442, 0.022746231, -0.035293855, -0.009433693) * g_20; result += mat4(0.015766997, 0.013647276, -0.029327558, 0.039106004, -0.010398323, -0.032851525, 0.02908329, -0.003789618, 0.12963496, 0.010851003, 0.1126276, -0.049255487, 0.06867432, 0.07970792, 0.017840397, -0.026481882) * g_21; result += mat4(-0.058729574, -0.07886952, 0.033267397, 0.02755372, -0.0172006, 0.012404398, -0.0230168, -0.015059758, -0.09239916, -0.029533267, -0.043251917, 0.0035152994, 0.022931995, 0.101714484, -0.044946067, 0.094993) * g_22; result += mat4(-0.04708704, -0.032475296, -0.03228093, -0.08810475, 0.013745045, 0.027828002, -0.031922746, 0.022986397, -0.061620213, -0.03694645, -0.055026993, 0.0031291894, -0.028799903, -0.0025357977, -0.03441407, 0.0028600092) * g_23; result += mat4(0.058981724, -0.10447273, -0.088705614, 0.16546178, -0.023549391, -0.008831522, -0.018411588, 0.029640056, -0.068086684, -0.05414636, -0.029401174, 0.036180343, -0.031988926, -0.047249753, 0.008162177, 0.00548062) * g_24; result += mat4(0.05287462, -0.030657746, 0.02821435, 0.037005343, 0.03534311, -0.15614955, 0.07085459, -0.11997641, -0.009156166, -0.021968868, -0.054147746, -0.07307657, -0.006428544, -0.017528288, 0.012614676, 0.037840024) * g_25; result += mat4(-0.021977803, 0.047799855, 0.02660416, -0.07292106, 0.045195807, -0.0056674764, 0.10824326, -0.112114795, 0.1447127, -0.0119616175, 0.0011661504, -0.04553905, 0.13048342, 0.14574122, -0.105522245, -0.102792375) * g_26; result += mat4(-0.16397473, 0.15785863, -0.06666504, -0.01682913, 0.06070918, 0.070222184, 0.037701584, 0.026657054, -0.0835267, -0.009457008, 0.13232987, 0.13508691, -0.056414206, -0.06818828, 0.079076104, 0.032249212) * g_27; result += vec4(-0.10795144, -0.09953324, -0.055413827, -0.03875493); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_last_tf1 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) vec4 hook() { vec4 result = mat4(0.024905335, -0.0020974763, 0.02695263, 0.00016802056, -0.024053082, -0.02133723, -0.031614035, -0.031826317, 0.120421864, 0.10555479, 0.08609448, 0.116875134, 0.046175968, 0.04224941, 0.059216674, 0.035143953) * g_0; result += mat4(0.059397914, 0.016519934, 0.07189327, 0.047407165, 0.04808963, 0.02792908, 0.057017103, 0.034324065, 0.14228246, 0.11275426, 0.088058695, 0.059600517, 0.02063494, 0.052596953, 0.047207687, 0.08789091) * g_1; result += mat4(-0.013453174, 0.008474715, -0.017593835, 0.009218917, 0.070580654, 0.040542338, 0.08812338, 0.074653216, -0.016356857, 0.015809007, -0.008739107, 0.0097674895, -0.018381525, -0.007775341, -0.040571664, -0.011188163) * g_2; result += mat4(-0.026196122, -0.034825727, -0.042998232, -0.033436514, -0.01678153, -0.004592797, -0.010311677, 0.0008815291, -0.08899181, -0.10274026, -0.066960976, -0.082430154, -0.057137426, -0.07554528, -0.030993424, -0.050372377) * g_3; result += mat4(0.022921838, -0.010479244, -0.050794605, -0.073633075, -0.053708922, 0.009594084, -0.071259, -0.01054356, 0.005165821, -0.08024963, -0.049251772, -0.09581235, 0.17995799, 0.09743011, 0.13533138, 0.11643848) * g_4; result += mat4(0.09727046, 0.07292666, 0.06820908, 0.041535784, -0.0049705, 0.0048759184, -0.035702795, -0.015944308, -0.010730028, 0.018847652, 0.06466244, 0.086318985, -0.05661574, -0.040698618, 0.010839972, 0.0027009705) * g_5; result += mat4(-0.04628466, 0.010060396, 0.02609333, 0.08664702, 0.057045907, 0.033591177, 0.02186063, -0.024303377, 0.006569828, 0.08025825, 0.016128821, 0.10180713, -0.12228169, -0.112990454, -0.078443415, -0.09126021) * g_6; result += mat4(-0.12733299, -0.087755, -0.07374111, -0.044979006, -0.025347412, -0.004083168, 0.023782173, 0.02900392, -0.017815407, -0.041119996, -0.057978686, -0.13521095, 0.08364004, 0.06950181, 0.023554614, 0.008043734) * g_7; result += mat4(0.009062775, -0.003570175, -0.007378757, -0.0018487388, 0.01145638, 0.05217187, -0.008250244, 0.008433307, -0.056756936, -0.044681005, -0.08096105, -0.08033185, -0.023784965, -0.01859799, 0.013042476, 0.021188647) * g_8; result += mat4(-0.0071619656, -0.012498299, -0.05144986, -0.078112476, -0.034992415, -0.017038302, -0.04464615, -0.044504963, 0.024249, -0.004297534, 0.03674578, 0.03090718, 0.04698553, 0.008344952, 0.057619847, -0.0338724) * g_9; result += mat4(-0.011845145, -0.0045043705, -1.6646482e-06, -0.0038495932, -0.01992515, 0.004827126, 0.019493148, 0.00862289, 0.10151322, 0.0021909082, 0.09940764, 0.03728846, 0.027824005, 0.04358071, 0.014909185, 0.036326095) * g_10; result += mat4(0.022513246, 0.028257169, 0.0102195935, 0.03301329, 0.052253865, -0.0021944977, 0.08247392, 0.03256867, -0.040685873, -0.0052207555, -0.0451257, -0.054165114, 0.01647699, 0.0028809097, -0.015233776, -0.0008741886) * g_11; result += mat4(0.017371105, 0.01597189, -0.052552313, -0.008554715, -0.0023150423, 0.006076517, -0.012868931, 0.0039361073, -0.007524978, -0.004284313, -0.021520883, -0.010327569, 0.02543678, 0.008725823, -0.0073885336, 0.005528395) * g_12; result += mat4(0.019192757, 0.016561812, 0.0027538154, 0.0013078215, 0.007916496, -0.042525183, -0.013173432, -0.05265476, -0.062195376, -0.011255499, 0.020898128, 0.021532273, -0.001524097, 0.034835674, -0.004051403, -0.0292426) * g_13; result += mat4(-0.049191684, -9.43322e-06, -0.009106849, 0.012845289, -0.019482708, -0.011163468, 0.0034011535, -0.007062845, -0.006469714, 0.03177786, -0.033006195, -0.0006813464, -0.053963087, 0.00085209147, 0.02734121, 0.034086403) * g_14; result += mat4(-0.03232248, -0.004037002, -0.010319106, 0.030889064, 0.019604538, 0.0020888883, 0.010277864, 0.000661223, 0.057915937, 0.030683514, 0.00042533095, -0.013019287, -0.015896408, 0.0038484468, -0.0042103594, 0.02174542) * g_15; result += mat4(0.032975145, 0.0011456647, 0.04913679, -0.017063798, 0.0117176045, 0.007440557, 0.0020480808, 0.009415731, 0.027573857, 0.015140836, -0.01679426, -0.006124731, -0.03206279, -0.029842237, -0.010428016, -0.028513178) * g_16; result += mat4(-0.00506859, 0.055869613, 0.010164368, 0.027031485, 0.042289548, -0.0054258504, 0.032214936, -0.029970925, -0.0058315448, 0.022889478, 0.01681123, 0.02985076, -0.111186065, -0.02202099, 0.0030994313, -0.062343158) * g_17; result += mat4(-0.060951103, 0.06079555, -0.0396464, 0.070911355, -0.011480358, -0.06803282, 0.01637355, -0.043100975, -0.00423709, -0.028337711, 0.021635853, 0.0014857082, 0.030084312, 0.018155476, 0.043694943, 0.038795974) * g_18; result += mat4(-0.0060662925, 0.029721662, -0.008117774, 0.034551267, -0.024477571, 0.018841071, -0.027095588, 0.034495078, 0.082398005, 0.008998768, -0.016399248, -0.043801688, 0.05936684, 0.006066549, 0.045399766, 3.5319943e-05) * g_19; result += mat4(0.019259382, 0.02494012, 0.029301709, 0.028329274, 0.09122267, 0.06900443, 0.1412115, -0.043169618, -0.01627418, -0.004989528, -0.0042651827, -0.04556752, -0.023623291, 0.013007996, -0.04483056, -0.015727345) * g_20; result += mat4(0.016332543, 0.016384754, -0.030676385, 0.045312885, -0.0100853555, -0.032632045, 0.031514473, -0.0070776115, 0.13642761, 0.0023589598, 0.12214136, -0.062155515, 0.08240989, 0.08894205, 0.03325406, -0.016589595) * g_21; result += mat4(-0.06494277, -0.08158925, 0.030425413, 0.019835634, -0.012624623, 0.013942616, -0.030527417, -0.021668324, -0.09444672, -0.033064254, -0.044167448, 0.0011024752, 0.03210801, 0.12662941, -0.03912534, 0.1112649) * g_22; result += mat4(-0.04716062, -0.03751481, -0.031030515, -0.09067383, 0.0077815712, 0.02169541, -0.035285182, 0.02290573, -0.0704085, -0.03916127, -0.058103334, 0.004915147, -0.0333844, -0.011548617, -0.031151932, -0.00043817286) * g_23; result += mat4(0.05976319, -0.107285, -0.097245865, 0.17706421, -0.021453341, -0.0047738464, -0.017621001, 0.033400454, -0.07225561, -0.05599672, -0.027600193, 0.038664024, -0.03762786, -0.052429967, 0.0104017975, 0.007116869) * g_24; result += mat4(0.06014114, -0.029824806, 0.03209269, 0.04392036, 0.031300627, -0.16249833, 0.06878509, -0.12658615, -0.012383169, -0.025043553, -0.06527381, -0.08149099, -0.014006842, -0.018669648, 0.014510818, 0.042045828) * g_25; result += mat4(-0.023342922, 0.047104675, 0.029629575, -0.082307704, 0.04035797, -0.0013049254, 0.11085582, -0.11031226, 0.14778149, -0.016699014, -0.00634342, -0.055320874, 0.14306462, 0.15896587, -0.110229075, -0.1069649) * g_26; result += mat4(-0.17449625, 0.15787153, -0.06711028, -0.023110518, 0.06862914, 0.074063435, 0.042682912, 0.029800726, -0.08768606, -0.009814701, 0.14180017, 0.14780663, -0.05672417, -0.074305914, 0.07873489, 0.028458012) * g_27; result += vec4(0.06026231, 0.040204916, 0.037672628, 0.023496555); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_last_tf2 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) vec4 hook() { vec4 result = mat4(0.1765669, 0.14268716, 0.19186598, 0.15799578, 0.016374417, 0.018578433, 0.0039475, 0.0046772263, 0.39840183, 0.36909792, 0.35409746, 0.37422222, -0.108508386, -0.1331279, -0.10336035, -0.14776541) * g_0; result += mat4(-0.057757027, -0.14071062, -0.025283009, -0.09397916, -0.09031894, -0.14219165, -0.08299535, -0.13970287, -0.12259208, -0.14382727, -0.22002274, -0.25016093, -0.048906635, 0.06620249, 0.016965045, 0.1295978) * g_1; result += mat4(-0.16748372, -0.13718611, -0.18565705, -0.15029612, -0.080749065, -0.09955825, 0.032431383, 0.023855643, -0.2748885, -0.23232168, -0.29121292, -0.26405892, 0.16556135, 0.18657646, 0.1424068, 0.18855052) * g_2; result += mat4(0.10960496, 0.10851629, 0.095003806, 0.11053746, 0.09885307, 0.14437789, 0.13191165, 0.17365928, 0.16558935, 0.15473324, 0.21136154, 0.19976667, -0.07267957, -0.11469687, -0.029134216, -0.06817615) * g_3; result += mat4(0.10202856, 0.04216857, -0.03959349, -0.09849683, -0.1576996, -0.049997438, -0.1579918, -0.058789205, 0.029792828, -0.07311781, -0.045432188, -0.11312683, 0.24257647, 0.16204113, 0.17869382, 0.16024388) * g_4; result += mat4(0.17193612, 0.12692013, 0.13177487, 0.0796725, 0.0797928, 0.08952722, -0.012468046, 0.011071511, -0.068559825, -0.024852324, 0.0526428, 0.07917346, -0.085534215, -0.09591339, 0.04615827, 0.024577664) * g_5; result += mat4(-0.14653449, -0.067267366, -0.002524394, 0.086243175, 0.13660401, 0.08039592, 0.09179008, 0.022573143, -0.024744196, 0.09120211, 0.017654825, 0.14114714, -0.16093308, -0.14538004, -0.09950235, -0.111152865) * g_6; result += mat4(-0.188637, -0.12968326, -0.1200479, -0.06537649, -0.12589337, -0.106242515, -0.02788782, -0.025949068, 0.04948153, 0.02222735, -0.025291357, -0.12379292, 0.11074645, 0.11902375, -0.00056989543, -0.0024386419) * g_7; result += mat4(0.018286629, 0.0072215167, 0.00037828335, 0.0047001047, 0.011478272, 0.041745186, -0.015742473, -0.002282524, -0.03440817, -0.02196847, -0.07838253, -0.07993771, -0.010155526, -0.017590692, 0.027141469, 0.029741213) * g_8; result += mat4(0.016512005, 0.004950637, -0.0238836, -0.05587327, -0.03164328, -0.009499985, -0.059880238, -0.061794154, 0.023154303, -0.013266373, 0.04701534, 0.0415862, 0.06357814, 0.033057794, 0.08389772, 0.00035060212) * g_9; result += mat4(-0.016403968, -0.012538788, -0.0015746636, -0.004771009, -0.021361275, -0.009695242, 0.020548422, -0.0024130535, 0.07796766, -0.01516671, 0.09961382, 0.042754963, 0.017363647, 0.03729065, -0.004795824, 0.01550197) * g_10; result += mat4(-0.0028093113, 0.011869523, -0.02216933, 0.011177349, 0.033342455, -0.021146454, 0.07830085, 0.032490104, -0.03281833, 0.0060484232, -0.04081057, -0.04945058, -0.0056189033, -0.010636801, -0.041949317, -0.025739705) * g_11; result += mat4(0.012979897, 0.016758928, -0.049062215, -0.0035748442, 0.0085972, 0.0036381132, -0.0055621094, 0.0041307937, -0.0008907763, -0.0034079372, -0.025680453, -0.015531803, 0.012816766, 0.009977763, -0.016416566, 0.0034859509) * g_12; result += mat4(0.021753248, 0.016452711, 0.009833835, 0.0065052663, 0.0014061348, -0.046160888, -0.0132271005, -0.05051269, -0.05746351, -0.0012690664, 0.017191738, 0.018192926, -0.008879476, 0.026354216, -0.012801991, -0.029587373) * g_13; result += mat4(-0.04220692, -0.0015560482, -0.0019648245, 0.013402305, -0.018259782, -0.0036008905, 0.0035650074, -0.0019178417, 0.00051580026, 0.027355857, -0.017914988, 0.004937948, -0.046335887, 0.00013612259, 0.030293299, 0.030688645) * g_14; result += mat4(-0.036683388, -0.0031274238, -0.026074665, 0.021684237, 0.022639066, 0.0022493738, 0.011508554, -0.0006385944, 0.04890418, 0.020119468, 0.004167364, -0.008356099, -0.008598796, 0.0089028, -0.0029575853, 0.016687104) * g_15; result += mat4(0.027207986, 0.0011099194, 0.042383645, -0.015179333, 0.014744431, 0.006148344, 0.005165422, 0.0070196544, 0.030286826, 0.016620956, -0.01611366, -0.00667594, -0.029524863, -0.024751091, -0.013321004, -0.025199674) * g_16; result += mat4(0.0027477827, 0.054622147, 0.010154094, 0.025437292, 0.031773083, -0.01055473, 0.022864206, -0.029010754, -0.0029999653, 0.025018329, 0.015316208, 0.027188798, -0.10096525, -0.017268656, 0.0012529213, -0.062078856) * g_17; result += mat4(-0.053670805, 0.057336535, -0.037418038, 0.06443577, -0.016027879, -0.058168363, 0.007034215, -0.03390141, -0.0019346164, -0.027947908, 0.021723913, -0.0018286633, 0.030507812, 0.018293543, 0.042917266, 0.033528328) * g_18; result += mat4(-0.004559579, 0.029667616, -0.001870353, 0.0378995, -0.017147437, 0.020192018, -0.021574946, 0.031568103, 0.07487145, 0.0032376775, -0.018893708, -0.041981626, 0.054478757, 0.0061423797, 0.041280247, 0.000878061) * g_19; result += mat4(0.017076394, 0.023647636, 0.029403262, 0.029923365, 0.08866472, 0.060613394, 0.1314274, -0.04490231, -0.016304834, -0.0062647443, -0.0031828512, -0.03989252, -0.024330825, 0.00741213, -0.04075287, -0.01615817) * g_20; result += mat4(0.017866978, 0.017720113, -0.02846163, 0.040761847, -0.0063438355, -0.02347501, 0.029564403, -0.0029562064, 0.12505588, -0.0073986333, 0.11250363, -0.06179967, 0.07854423, 0.08546533, 0.034743227, -0.010757377) * g_21; result += mat4(-0.06416677, -0.08344284, 0.030138884, 0.017635904, -0.012087523, 0.014205202, -0.03221233, -0.023834767, -0.091186255, -0.028958676, -0.04724334, 0.00013161585, 0.027391518, 0.1249978, -0.045047652, 0.10737729) * g_22; result += mat4(-0.04326348, -0.03543181, -0.029558217, -0.08582413, 0.007812453, 0.014296562, -0.028779754, 0.018517692, -0.063755795, -0.036619596, -0.050809663, 0.005431336, -0.029205568, -0.011827915, -0.031110523, -0.005648626) * g_23; result += mat4(0.05499293, -0.10000709, -0.0943537, 0.16143042, -0.019952895, -0.0039807972, -0.014841254, 0.0320363, -0.065173544, -0.049425576, -0.023904482, 0.03759679, -0.03207411, -0.047782745, 0.01352581, 0.008140566) * g_24; result += mat4(0.055923894, -0.025134467, 0.029583648, 0.04096879, 0.027551858, -0.14995384, 0.06467113, -0.11633077, -0.01563784, -0.026909819, -0.06292879, -0.078409635, -0.009081105, -0.015533088, 0.019585673, 0.04334208) * g_25; result += mat4(-0.021717606, 0.042464726, 0.02743202, -0.07388838, 0.03460472, 0.0038285658, 0.099842004, -0.098247, 0.13276267, -0.020793032, -0.008603039, -0.051913783, 0.12959045, 0.14735717, -0.10888226, -0.10263746) * g_26; result += mat4(-0.16819532, 0.141579, -0.062480718, -0.021918943, 0.06348125, 0.06849444, 0.03888676, 0.027375204, -0.08194279, -0.012574497, 0.13523251, 0.13739482, -0.047547445, -0.058767617, 0.07009549, 0.028136581) * g_27; result += vec4(0.069033325, 0.040207114, 0.027286075, 0.0065334598); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Depth-to-Space //!HOOK MAIN //!BIND MAIN //!BIND conv2d_last_tf //!BIND conv2d_last_tf1 //!BIND conv2d_last_tf2 //!SAVE MAIN //!WIDTH conv2d_last_tf.w 2 * //!HEIGHT conv2d_last_tf.h 2 * //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * vec4 hook() { vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size); ivec2 i0 = ivec2(f0 * vec2(2.0)); float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x]; vec2 f1 = fract(conv2d_last_tf1_pos * conv2d_last_tf1_size); ivec2 i1 = ivec2(f1 * vec2(2.0)); float c1 = conv2d_last_tf1_tex((vec2(0.5) - f1) * conv2d_last_tf1_pt + conv2d_last_tf1_pos)[i1.y * 2 + i1.x]; vec2 f2 = fract(conv2d_last_tf2_pos * conv2d_last_tf2_size); ivec2 i2 = ivec2(f2 * vec2(2.0)); float c2 = conv2d_last_tf2_tex((vec2(0.5) - f2) * conv2d_last_tf2_pt + conv2d_last_tf2_pos)[i2.y * 2 + i2.x]; float c3 = c2; return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/LICENSE ================================================ MIT License Copyright (c) 2019 bloc97 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: distribute_options.yaml ================================================ output: dist/ ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) # Start of the permission_handler configuration target.build_configurations.each do |config| # You can enable the permissions needed here. For example to enable camera # permission, just remove the `#` character in front so it looks like this: # # ## dart: PermissionGroup.camera # 'PERMISSION_CAMERA=1' # # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.calendar # 'PERMISSION_EVENTS=1', ## dart: PermissionGroup.reminders # 'PERMISSION_REMINDERS=1', ## dart: PermissionGroup.contacts # 'PERMISSION_CONTACTS=1', ## dart: PermissionGroup.camera # 'PERMISSION_CAMERA=1', ## dart: PermissionGroup.microphone # 'PERMISSION_MICROPHONE=1', ## dart: PermissionGroup.speech # 'PERMISSION_SPEECH_RECOGNIZER=1', ## dart: PermissionGroup.photos 'PERMISSION_PHOTOS=1', ## dart: PermissionGroup.photosAddOnly # 'PERMISSION_PHOTOS_ADD_ONLY =1', ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] # 'PERMISSION_LOCATION=1', ## dart: PermissionGroup.notification # 'PERMISSION_NOTIFICATIONS=1', ## dart: PermissionGroup.mediaLibrary # 'PERMISSION_MEDIA_LIBRARY=1', ## dart: PermissionGroup.sensors # 'PERMISSION_SENSORS=1', ## dart: PermissionGroup.bluetooth # 'PERMISSION_BLUETOOTH=1', ## dart: PermissionGroup.appTrackingTransparency # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', ## dart: PermissionGroup.criticalAlerts # 'PERMISSION_CRITICAL_ALERTS=1' ] end # End of the permission_handler configuration end end ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import Flutter import UIKit @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { application.applicationSupportsShakeToEdit = false // Disable shake to undo return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} ================================================ FILE: ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json ================================================ { "images" : [ { "filename" : "background.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "darkbackground.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "filename" : "LaunchImage.png", "idiom" : "universal", "scale" : "1x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "LaunchImage@2x.png", "idiom" : "universal", "scale" : "2x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "LaunchImage@3x.png", "idiom" : "universal", "scale" : "3x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. 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. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneClassName UIWindowScene UISceneDelegateClassName FlutterSceneDelegate UISceneConfigurationName flutter UISceneStoryboardFile Main FlutterDeepLinkingEnabled CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName PiliPlus CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName PiliPlus CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents NSPhotoLibraryUsageDescription 请允许APP保存图片到相册 NSCameraUsageDescription App需要您的同意,才能访问相册 NSAppleMusicUsageDescription App需要您的同意,才能访问媒体资料库 LSApplicationQueriesSchemes https http CFBundleURLTypes CFBundleURLName CFBundleURLSchemes http https CFBundleURLTypes CFBundleURLName CFBundleURLSchemes m.bilibili.com bilibili.com www.bilibili.com bangumi.bilibili.com bilibili.cn www.bilibili.cn bangumi.bilibili.cn bilibili.tv www.bilibili.tv bangumi.bilibili.tv miniapp.bilibili.com live.bilibili.com CFBundleURLName bilibili CFBundleURLSchemes bilibili UIBackgroundModes audio UIStatusBarHidden ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 2431C9E3151C7449D0D1C0F4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63F2789C7C1DF3CD2B80A936 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 32E2926120A1A8DC0E629BC6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3DA6FBBC55FDD1E3261D6D67 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 51DB54E5BB66F8608325A082 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 63F2789C7C1DF3CD2B80A936 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 2431C9E3151C7449D0D1C0F4 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 31399C6148E878051E614578 /* Frameworks */ = { isa = PBXGroup; children = ( 63F2789C7C1DF3CD2B80A936 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, EFBDDDF3F822865E6D1BB6D7 /* Pods */, 31399C6148E878051E614578 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; EFBDDDF3F822865E6D1BB6D7 /* Pods */ = { isa = PBXGroup; children = ( 51DB54E5BB66F8608325A082 /* Pods-Runner.debug.xcconfig */, 3DA6FBBC55FDD1E3261D6D67 /* Pods-Runner.release.xcconfig */, 32E2926120A1A8DC0E629BC6 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 67CCBB29D5A20A144E2BDF7D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */, B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 67CCBB29D5A20A144E2BDF7D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.piliplus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.piliplus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.piliplus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: lib/build_config.dart ================================================ abstract final class BuildConfig { static const int versionCode = int.fromEnvironment( 'pili.code', defaultValue: 1, ); static const String versionName = String.fromEnvironment( 'pili.name', defaultValue: 'SNAPSHOT', ); static const int buildTime = int.fromEnvironment('pili.time'); static const String commitHash = String.fromEnvironment( 'pili.hash', defaultValue: 'N/A', ); } ================================================ FILE: lib/common/constants.dart ================================================ import 'package:flutter/material.dart'; abstract final class StyleString { static const double cardSpace = 8; static const double safeSpace = 12; static const BorderRadius mdRadius = BorderRadius.all(imgRadius); static const Radius imgRadius = Radius.circular(10); static const double aspectRatio = 16 / 10; static const double aspectRatio16x9 = 16 / 9; static const double imgMaxRatio = 2.6; static const bottomSheetRadius = BorderRadius.vertical( top: Radius.circular(18), ); static const dialogFixedConstraints = BoxConstraints( minWidth: 420, maxWidth: 420, ); static const topBarHeight = 52.0; static const buttonStyle = ButtonStyle( visualDensity: VisualDensity( horizontal: -2, vertical: -1.25, ), tapTargetSize: .shrinkWrap, ); } abstract final class Constants { static const appName = 'PiliPlus'; static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus'; // 27eb53fc9058f8c3 移动端 Android // 4409e2ce8ffd12b8 HD版 static const String appKey = 'dfca71928277209b'; // 59b43e04ad6965f34319062b478f83dd TV端 static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e'; // static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; // static const String thirdApi = // 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png'; static const String traceId = '11111111111111111111111111111111:1111111111111111:0:0'; static const String userAgent = 'Mozilla/5.0 BiliDroid/2.0.1 (bbcallen@gmail.com) os/android model/android_hd mobi_app/android_hd build/2001100 channel/master innerVer/2001100 osVer/15 network/2'; static const String statistics = '{"appId":5,"platform":3,"version":"2.0.1","abtest":""}'; // 请求时会自动encodeComponent // app static const String userAgentApp = 'Mozilla/5.0 BiliDroid/8.43.0 (bbcallen@gmail.com) os/android model/android mobi_app/android build/8430300 channel/master innerVer/8430300 osVer/15 network/2'; static const String statisticsApp = '{"appId":1,"platform":3,"version":"8.43.0","abtest":""}'; static const baseHeaders = { // 'referer': HttpString.baseUrl, 'env': 'prod', 'app-key': 'android64', 'x-bili-aurora-zone': 'sh001', }; static final urlRegex = RegExp( r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]', ); static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris"; // 'itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme,sunflowerStyle,cardsEnhance,eva3CardOpus,eva3CardVideo,eva3CardComment,eva3CardVote,eva3CardUser' static const dynFeatures = 'itemOpusStyle,listOnlyfans,onlyfansQaCard'; // 超分辨率滤镜 static const List mpvAnime4KShaders = [ 'Anime4K_Clamp_Highlights.glsl', 'Anime4K_Restore_CNN_VL.glsl', 'Anime4K_Upscale_CNN_x2_VL.glsl', 'Anime4K_AutoDownscalePre_x2.glsl', 'Anime4K_AutoDownscalePre_x4.glsl', 'Anime4K_Upscale_CNN_x2_M.glsl', ]; // 超分辨率滤镜 (轻量) static const mpvAnime4KShadersLite = [ 'Anime4K_Clamp_Highlights.glsl', 'Anime4K_Restore_CNN_M.glsl', 'Anime4K_Restore_CNN_S.glsl', 'Anime4K_Upscale_CNN_x2_M.glsl', 'Anime4K_AutoDownscalePre_x2.glsl', 'Anime4K_AutoDownscalePre_x4.glsl', 'Anime4K_Upscale_CNN_x2_S.glsl', ]; //内容来自 https://passport.bilibili.com/web/generic/country/list static const internationalDialingPrefix = [ (id: 1, cname: "中国大陆", countryId: 86), (id: 5, cname: "中国香港特别行政区", countryId: 852), (id: 2, cname: "中国澳门特别行政区", countryId: 853), (id: 3, cname: "中国台湾", countryId: 886), (id: 4, cname: "美国", countryId: 1), (id: 6, cname: "比利时", countryId: 32), (id: 7, cname: "澳大利亚", countryId: 61), (id: 8, cname: "法国", countryId: 33), (id: 9, cname: "加拿大", countryId: 1), (id: 10, cname: "日本", countryId: 81), (id: 11, cname: "新加坡", countryId: 65), (id: 12, cname: "韩国", countryId: 82), (id: 13, cname: "马来西亚", countryId: 60), (id: 14, cname: "英国", countryId: 44), (id: 15, cname: "意大利", countryId: 39), (id: 16, cname: "德国", countryId: 49), (id: 18, cname: "俄罗斯", countryId: 7), (id: 19, cname: "新西兰", countryId: 64), (id: 153, cname: "瓦利斯群岛和富图纳群岛", countryId: 1681), (id: 152, cname: "葡萄牙", countryId: 351), (id: 151, cname: "帕劳", countryId: 680), (id: 150, cname: "诺福克岛", countryId: 672), (id: 149, cname: "挪威", countryId: 47), (id: 148, cname: "纽埃岛", countryId: 683), (id: 147, cname: "尼日利亚", countryId: 234), (id: 146, cname: "尼日尔", countryId: 227), (id: 145, cname: "尼加拉瓜", countryId: 505), (id: 144, cname: "尼泊尔", countryId: 977), (id: 143, cname: "瑙鲁", countryId: 674), (id: 154, cname: "格鲁吉亚", countryId: 995), (id: 155, cname: "瑞典", countryId: 46), (id: 165, cname: "沙特阿拉伯", countryId: 966), (id: 164, cname: "桑给巴尔岛", countryId: 259), (id: 163, cname: "塞舌尔共和国", countryId: 248), (id: 162, cname: "塞浦路斯", countryId: 357), (id: 161, cname: "塞内加尔", countryId: 221), (id: 160, cname: "塞拉利昂", countryId: 232), (id: 159, cname: "萨摩亚,东部", countryId: 684), (id: 158, cname: "萨摩亚,西部", countryId: 685), (id: 157, cname: "萨尔瓦多", countryId: 503), (id: 156, cname: "瑞士", countryId: 41), (id: 166, cname: "圣多美和普林西比", countryId: 239), (id: 142, cname: "塞尔维亚", countryId: 381), (id: 141, cname: "南非", countryId: 27), (id: 128, cname: "毛里塔尼亚", countryId: 222), (id: 127, cname: "毛里求斯", countryId: 230), (id: 126, cname: "马歇尔岛", countryId: 692), (id: 125, cname: "马提尼克岛", countryId: 596), (id: 124, cname: "马其顿", countryId: 389), (id: 123, cname: "马里亚纳岛", countryId: 1670), (id: 122, cname: "马里", countryId: 223), (id: 121, cname: "马拉维", countryId: 265), (id: 120, cname: "马耳他", countryId: 356), (id: 119, cname: "马尔代夫", countryId: 960), (id: 129, cname: "蒙古", countryId: 976), (id: 130, cname: "蒙特塞拉特岛", countryId: 1664), (id: 140, cname: "纳米比亚", countryId: 264), (id: 139, cname: "墨西哥", countryId: 52), (id: 138, cname: "莫桑比克", countryId: 258), (id: 137, cname: "摩纳哥", countryId: 377), (id: 136, cname: "摩洛哥", countryId: 212), (id: 135, cname: "摩尔多瓦", countryId: 373), (id: 134, cname: "缅甸", countryId: 95), (id: 133, cname: "密克罗尼西亚", countryId: 691), (id: 132, cname: "秘鲁", countryId: 51), (id: 131, cname: "孟加拉国", countryId: 880), (id: 118, cname: "马达加斯加", countryId: 261), (id: 167, cname: "圣卢西亚", countryId: 1784), (id: 216, cname: "智利", countryId: 56), (id: 203, cname: "牙买加", countryId: 1876), (id: 202, cname: "叙利亚", countryId: 963), (id: 201, cname: "匈牙利", countryId: 36), (id: 200, cname: "科特迪瓦", countryId: 225), (id: 199, cname: "希腊", countryId: 30), (id: 198, cname: "西班牙", countryId: 34), (id: 197, cname: "乌兹别克斯坦", countryId: 998), (id: 196, cname: "乌拉圭", countryId: 598), (id: 195, cname: "乌克兰", countryId: 380), (id: 194, cname: "乌干达", countryId: 256), (id: 204, cname: "亚美尼亚", countryId: 374), (id: 205, cname: "也门", countryId: 967), (id: 215, cname: "直布罗陀", countryId: 350), (id: 214, cname: "乍得", countryId: 235), (id: 213, cname: "赞比亚", countryId: 260), (id: 212, cname: "越南", countryId: 84), (id: 211, cname: "约旦", countryId: 962), (id: 210, cname: "印尼", countryId: 62), (id: 209, cname: "印度", countryId: 91), (id: 208, cname: "以色列", countryId: 972), (id: 207, cname: "伊朗", countryId: 98), (id: 206, cname: "伊拉克", countryId: 964), (id: 193, cname: "文莱", countryId: 673), (id: 192, cname: "委内瑞拉", countryId: 58), (id: 191, cname: "维珍群岛(英属)", countryId: 1284), (id: 178, cname: "泰国", countryId: 66), (id: 177, cname: "索马里", countryId: 252), (id: 176, cname: "所罗门群岛", countryId: 677), (id: 175, cname: "苏里南", countryId: 597), (id: 174, cname: "苏丹", countryId: 249), (id: 173, cname: "斯威士兰", countryId: 268), (id: 172, cname: "斯洛文尼亚", countryId: 386), (id: 171, cname: "斯洛伐克", countryId: 421), (id: 170, cname: "斯里兰卡", countryId: 94), (id: 169, cname: "圣皮埃尔和密克隆群岛", countryId: 508), (id: 179, cname: "坦桑尼亚", countryId: 255), (id: 180, cname: "汤加", countryId: 676), (id: 190, cname: "维珍群岛(美属)", countryId: 1340), (id: 189, cname: "瓦努阿图", countryId: 678), (id: 188, cname: "托克劳岛", countryId: 690), (id: 187, cname: "土库曼斯坦", countryId: 993), (id: 186, cname: "土耳其", countryId: 90), (id: 185, cname: "图瓦卢", countryId: 688), (id: 184, cname: "突尼斯", countryId: 216), (id: 183, cname: "阿森松岛", countryId: 247), (id: 182, cname: "特立尼达和多巴哥", countryId: 1868), (id: 181, cname: "特克斯和凯科斯", countryId: 1649), (id: 168, cname: "圣马力诺", countryId: 378), (id: 67, cname: "法属圭亚那", countryId: 594), (id: 54, cname: "不丹", countryId: 975), (id: 53, cname: "博茨瓦纳", countryId: 267), (id: 52, cname: "伯利兹", countryId: 501), (id: 51, cname: "玻利维亚", countryId: 591), (id: 50, cname: "波兰", countryId: 48), (id: 49, cname: "波黑", countryId: 387), (id: 48, cname: "波多黎各", countryId: 1787), (id: 47, cname: "冰岛", countryId: 354), (id: 46, cname: "贝宁", countryId: 229), (id: 45, cname: "保加利亚", countryId: 359), (id: 55, cname: "布基纳法索", countryId: 226), (id: 56, cname: "布隆迪", countryId: 257), (id: 66, cname: "法属波利尼西亚", countryId: 689), (id: 65, cname: "法罗岛", countryId: 298), (id: 64, cname: "厄立特里亚", countryId: 291), (id: 63, cname: "厄瓜多尔", countryId: 593), (id: 62, cname: "多米尼加代表", countryId: 1809), (id: 61, cname: "多米尼加", countryId: 1767), (id: 60, cname: "多哥", countryId: 228), (id: 59, cname: "迪戈加西亚岛", countryId: 246), (id: 58, cname: "丹麦", countryId: 45), (id: 57, cname: "赤道几内亚", countryId: 240), (id: 44, cname: "百慕大群岛", countryId: 1441), (id: 43, cname: "白俄罗斯", countryId: 375), (id: 42, cname: "巴西", countryId: 55), (id: 29, cname: "爱尔兰", countryId: 353), (id: 28, cname: "埃塞俄比亚", countryId: 251), (id: 27, cname: "埃及", countryId: 20), (id: 26, cname: "阿塞拜疆", countryId: 994), (id: 25, cname: "阿曼", countryId: 968), (id: 24, cname: "阿联酋", countryId: 971), (id: 23, cname: "阿根廷", countryId: 54), (id: 22, cname: "阿富汗", countryId: 93), (id: 21, cname: "阿尔及利亚", countryId: 213), (id: 20, cname: "阿尔巴尼亚", countryId: 355), (id: 30, cname: "爱沙尼亚", countryId: 372), (id: 31, cname: "安道尔", countryId: 376), (id: 41, cname: "巴拿马", countryId: 507), (id: 40, cname: "巴林", countryId: 973), (id: 39, cname: "巴拉圭", countryId: 595), (id: 38, cname: "巴基斯坦", countryId: 92), (id: 37, cname: "巴哈马群岛", countryId: 1242), (id: 36, cname: "巴布亚新几内亚", countryId: 675), (id: 35, cname: "巴巴多斯", countryId: 1246), (id: 34, cname: "奥地利", countryId: 43), (id: 33, cname: "安提瓜岛和巴布达", countryId: 1268), (id: 32, cname: "安哥拉", countryId: 244), (id: 68, cname: "非洲中部", countryId: 236), (id: 117, cname: "罗马尼亚", countryId: 40), (id: 104, cname: "科威特", countryId: 965), (id: 103, cname: "科摩罗", countryId: 269), (id: 102, cname: "开曼群岛", countryId: 1345), (id: 101, cname: "卡塔尔", countryId: 974), (id: 100, cname: "喀麦隆", countryId: 237), (id: 99, cname: "聚会岛", countryId: 262), (id: 98, cname: "津巴布韦", countryId: 263), (id: 97, cname: "捷克", countryId: 420), (id: 96, cname: "柬埔寨", countryId: 855), (id: 95, cname: "加蓬", countryId: 241), (id: 105, cname: "克罗地亚", countryId: 385), (id: 106, cname: "肯尼亚", countryId: 254), (id: 116, cname: "卢旺达", countryId: 250), (id: 115, cname: "卢森堡", countryId: 352), (id: 114, cname: "利比亚", countryId: 218), (id: 113, cname: "利比里亚", countryId: 231), (id: 112, cname: "立陶宛", countryId: 370), (id: 111, cname: "黎巴嫩", countryId: 961), (id: 110, cname: "老挝", countryId: 856), (id: 109, cname: "莱索托", countryId: 266), (id: 108, cname: "拉脱维亚", countryId: 371), (id: 107, cname: "库克岛", countryId: 682), (id: 94, cname: "加纳", countryId: 233), (id: 93, cname: "几内亚比绍", countryId: 245), (id: 92, cname: "几内亚", countryId: 224), (id: 79, cname: "格林纳达", countryId: 1473), (id: 78, cname: "哥斯达黎加", countryId: 506), (id: 77, cname: "哥伦比亚", countryId: 57), (id: 76, cname: "刚果(金)", countryId: 243), (id: 75, cname: "刚果", countryId: 242), (id: 74, cname: "冈比亚", countryId: 220), (id: 73, cname: "福克兰岛", countryId: 500), (id: 72, cname: "佛得角", countryId: 238), (id: 71, cname: "芬兰", countryId: 358), (id: 70, cname: "斐济", countryId: 679), (id: 80, cname: "格陵兰岛", countryId: 299), (id: 81, cname: "古巴", countryId: 53), (id: 91, cname: "吉尔吉斯斯坦", countryId: 996), (id: 90, cname: "吉布提", countryId: 253), (id: 89, cname: "基里巴斯", countryId: 686), (id: 88, cname: "维克岛", countryId: 1808), (id: 87, cname: "洪都拉斯", countryId: 504), (id: 86, cname: "荷兰", countryId: 31), (id: 85, cname: "朝鲜", countryId: 850), (id: 84, cname: "海地", countryId: 509), (id: 83, cname: "关岛", countryId: 1671), (id: 82, cname: "瓜德罗普岛", countryId: 590), (id: 69, cname: "菲律宾", countryId: 63), ]; } ================================================ FILE: lib/common/skeleton/dynamic_card.dart ================================================ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:flutter/material.dart'; class DynamicCardSkeleton extends StatelessWidget { const DynamicCardSkeleton({super.key}); @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final color = theme.colorScheme.onInverseSurface; final buttonStyle = TextButton.styleFrom( tapTargetSize: .padded, padding: const .symmetric(horizontal: 15), foregroundColor: theme.colorScheme.outline.withValues( alpha: 0.2, ), ); return Skeleton( child: Container( padding: const EdgeInsets.only(left: 12, right: 12, top: 12), decoration: BoxDecoration( border: Border( bottom: BorderSide( width: 8, color: theme.dividerColor.withValues(alpha: 0.05), ), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.all(Radius.circular(20)), ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( color: color, width: 100, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( color: color, width: 50, height: 11, ), ], ), ], ), const SizedBox(height: 10), Container( color: color, width: double.infinity, height: 13, margin: const EdgeInsets.only(bottom: 7), ), Container( color: color, width: double.infinity, height: 13, margin: const EdgeInsets.only(bottom: 7), ), Container( color: color, width: 300, height: 13, margin: const EdgeInsets.only(bottom: 7), ), Container( color: color, width: 250, height: 13, margin: const EdgeInsets.only(bottom: 7), ), Container( color: color, width: 100, height: 13, margin: const EdgeInsets.only(bottom: 7), ), if (GlobalData().dynamicsWaterfallFlow) const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: const ['转发', '评论', '点赞'] .map( (e) => TextButton.icon( onPressed: () {}, icon: const Icon( Icons.radio_button_unchecked_outlined, size: 20, ), style: buttonStyle, label: Text(e), ), ) .toList(), ), ], ), ), ); } } ================================================ FILE: lib/common/skeleton/fav_pgc_item.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:flutter/material.dart' hide LayoutBuilder; class FavPgcItemSkeleton extends StatelessWidget { const FavPgcItemSkeleton({super.key}); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace, vertical: 5, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 3 / 4, child: LayoutBuilder( builder: (context, boxConstraints) { return Container( decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.all(Radius.circular(4)), ), width: boxConstraints.maxWidth, height: boxConstraints.maxHeight, ); }, ), ), const SizedBox(width: 10), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 175, height: 12, color: color, ), const SizedBox(height: 10), Container( width: 55, height: 11, color: color, ), const SizedBox(height: 5), Container( width: 35, height: 11, color: color, ), ], ), ), ], ), ), ); } } ================================================ FILE: lib/common/skeleton/media_bangumi.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class MediaPgcSkeleton extends StatefulWidget { const MediaPgcSkeleton({super.key}); @override State createState() => _MediaPgcSkeletonState(); } class _MediaPgcSkeletonState extends State { @override Widget build(BuildContext context) { Color bgColor = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Padding( padding: const EdgeInsets.fromLTRB( StyleString.safeSpace, 7, StyleString.safeSpace, 7, ), child: Row( children: [ Container( width: 111, height: 148, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(6)), color: bgColor, ), ), const SizedBox(width: 10), Expanded( child: SizedBox( height: 148, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( color: bgColor, width: 200, height: 20, margin: const EdgeInsets.only(bottom: 15), ), Container( color: bgColor, width: 150, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( color: bgColor, width: 150, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( color: bgColor, width: 150, height: 13, ), const Spacer(), Container( width: 90, height: 35, decoration: BoxDecoration( borderRadius: const BorderRadius.all( Radius.circular(20), ), color: bgColor, ), ), ], ), ), ), ], ), ), ); } } ================================================ FILE: lib/common/skeleton/msg_feed_sys_msg_.dart ================================================ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class MsgFeedSysMsgSkeleton extends StatelessWidget { const MsgFeedSysMsgSkeleton({super.key}); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 125, height: 16, color: color, ), const SizedBox(height: 6), Container( width: double.infinity, height: 12, color: color, ), const SizedBox(height: 4), Container( width: double.infinity, height: 12, color: color, ), const SizedBox(height: 4), Container( width: 100, height: 12, color: color, ), const SizedBox(height: 4), Align( alignment: Alignment.centerRight, child: Container( width: 100, height: 10, color: color, ), ), ], ), ), ); } } ================================================ FILE: lib/common/skeleton/msg_feed_top.dart ================================================ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class MsgFeedTopSkeleton extends StatelessWidget { const MsgFeedTopSkeleton({super.key}); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: ListTile( leading: Container( width: 45, height: 45, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), title: UnconstrainedBox( alignment: Alignment.centerLeft, child: Container( width: 100, height: 11, color: color, ), ), subtitle: Container( color: color, width: 125, height: 11, ), ), ); } } ================================================ FILE: lib/common/skeleton/skeleton.dart ================================================ import 'package:flutter/material.dart'; class Skeleton extends StatelessWidget { final Widget child; const Skeleton({ required this.child, super.key, }); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.surface.withAlpha(10); final shimmerGradient = LinearGradient( colors: [ Colors.transparent, color, color, Colors.transparent, ], stops: const [ 0.1, 0.3, 0.5, 0.7, ], begin: const Alignment(-1.0, -0.3), end: const Alignment(1.0, 0.9), tileMode: TileMode.clamp, ); return Shimmer( linearGradient: shimmerGradient, child: ShimmerLoading( isLoading: true, child: child, ), ); } } class Shimmer extends StatefulWidget { static ShimmerState? of(BuildContext context) { return context.findAncestorStateOfType(); } const Shimmer({ super.key, required this.linearGradient, this.child, }); final LinearGradient linearGradient; final Widget? child; @override ShimmerState createState() => ShimmerState(); } class ShimmerState extends State with SingleTickerProviderStateMixin { late AnimationController _shimmerController; @override void initState() { super.initState(); _shimmerController = AnimationController.unbounded(vsync: this) ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000)); } @override void dispose() { _shimmerController.dispose(); super.dispose(); } LinearGradient get gradient => LinearGradient( colors: widget.linearGradient.colors, stops: widget.linearGradient.stops, begin: widget.linearGradient.begin, end: widget.linearGradient.end, transform: _SlidingGradientTransform( slidePercent: _shimmerController.value, ), ); bool get isSized => (context.findRenderObject() as RenderBox?)?.hasSize ?? false; Size get size => (context.findRenderObject() as RenderBox).size; Offset getDescendantOffset({ required RenderBox descendant, Offset offset = Offset.zero, }) { final shimmerBox = context.findRenderObject() as RenderBox; return descendant.localToGlobal(offset, ancestor: shimmerBox); } Listenable get shimmerChanges => _shimmerController; @override Widget build(BuildContext context) { return widget.child ?? const SizedBox.shrink(); } } class _SlidingGradientTransform extends GradientTransform { const _SlidingGradientTransform({ required this.slidePercent, }); final double slidePercent; @override Matrix4? transform(Rect bounds, {TextDirection? textDirection}) { return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0); } } class ShimmerLoading extends StatefulWidget { const ShimmerLoading({ super.key, required this.isLoading, required this.child, }); final bool isLoading; final Widget child; @override State createState() => _ShimmerLoadingState(); } class _ShimmerLoadingState extends State { Listenable? _shimmerChanges; @override void didChangeDependencies() { super.didChangeDependencies(); if (_shimmerChanges != null) { _shimmerChanges!.removeListener(_onShimmerChange); } _shimmerChanges = Shimmer.of(context)?.shimmerChanges; if (_shimmerChanges != null) { _shimmerChanges!.addListener(_onShimmerChange); } } @override void dispose() { _shimmerChanges?.removeListener(_onShimmerChange); super.dispose(); } void _onShimmerChange() { if (widget.isLoading) { setState(() {}); } } @override Widget build(BuildContext context) { if (!widget.isLoading) { return widget.child; } final shimmer = Shimmer.of(context)!; if (!shimmer.isSized) { return const SizedBox.shrink(); } final shimmerSize = shimmer.size; final gradient = shimmer.gradient; final offsetWithinShimmer = shimmer.getDescendantOffset( descendant: context.findRenderObject() as RenderBox, ); return ShaderMask( blendMode: BlendMode.srcATop, shaderCallback: (bounds) { return gradient.createShader( Rect.fromLTWH( -offsetWithinShimmer.dx, -offsetWithinShimmer.dy, shimmerSize.width, shimmerSize.height, ), ); }, child: widget.child, ); } } ================================================ FILE: lib/common/skeleton/space_opus.dart ================================================ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide LayoutBuilder; class SpaceOpusSkeleton extends StatelessWidget { const SpaceOpusSkeleton({super.key}); @override Widget build(BuildContext context) { final surface = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Card( clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(6)), ), child: LayoutBuilder( builder: (context, constraints) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: (0.68 + 0.82 * Utils.random.nextDouble()) * constraints.maxWidth, color: surface, ), Container( height: 10, color: surface, margin: const EdgeInsets.all(10), width: constraints.maxWidth * 0.7, ), Container( height: 10, color: surface, margin: const EdgeInsets.only( left: 10, right: 10, bottom: 10, ), width: constraints.maxWidth, ), ], ); }, ), ), ); } } ================================================ FILE: lib/common/skeleton/video_card_h.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class VideoCardHSkeleton extends StatelessWidget { const VideoCardHSkeleton({super.key}); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace, vertical: 5, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: StyleString.aspectRatio, child: DecoratedBox( decoration: BoxDecoration( color: color, borderRadius: StyleString.mdRadius, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(10, 4, 6, 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( color: color, width: 200, height: 11, margin: const EdgeInsets.only(bottom: 5), ), Container( color: color, width: 150, height: 13, ), const Spacer(), Container( color: color, width: 100, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Row( children: [ Container( color: color, width: 40, height: 13, margin: const EdgeInsets.only(right: 8), ), Container( color: color, width: 40, height: 13, ), ], ), ], ), ), ), ], ), ), ); } } ================================================ FILE: lib/common/skeleton/video_card_v.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class VideoCardVSkeleton extends StatelessWidget { const VideoCardVSkeleton({super.key}); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: StyleString.aspectRatio, child: DecoratedBox( decoration: BoxDecoration( color: color, borderRadius: StyleString.mdRadius, ), ), ), Padding( // 多列 padding: const EdgeInsets.fromLTRB(4, 5, 6, 6), // 单列 // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // const SizedBox(height: 6), Container( width: 200, height: 13, margin: const EdgeInsets.only(bottom: 5), color: color, ), Container( width: 150, height: 13, margin: const EdgeInsets.only(bottom: 12), color: color, ), Container( width: 110, height: 13, margin: const EdgeInsets.only(bottom: 5), color: color, ), Container( width: 75, height: 13, color: color, ), ], ), ), ], ), ); } } ================================================ FILE: lib/common/skeleton/video_reply.dart ================================================ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class VideoReplySkeleton extends StatelessWidget { const VideoReplySkeleton({super.key}); @override Widget build(BuildContext context) { Color bgColor = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 8, 8, 2), child: Row( children: [ ClipOval( child: Container( width: 34, height: 34, color: bgColor, ), ), const SizedBox(width: 12), Container( width: 80, height: 13, color: bgColor, ), ], ), ), Padding( padding: const EdgeInsets.only( top: 4, left: 57, right: 6, bottom: 6, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 300, height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), Container( width: 180, height: 14, margin: const EdgeInsets.only(bottom: 10), color: bgColor, ), Row( children: [ Container( width: 40, height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), const Spacer(), Container( width: 30, height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), const SizedBox(width: 8), Container( width: 30, height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), const SizedBox(width: 8), ], ), ], ), ), ], ), ); } } ================================================ FILE: lib/common/skeleton/whisper_item.dart ================================================ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:flutter/material.dart'; class WhisperItemSkeleton extends StatelessWidget { const WhisperItemSkeleton({super.key}); @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.onInverseSurface; return Skeleton( child: ListTile( leading: Container( width: 45, height: 45, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), title: UnconstrainedBox( alignment: Alignment.centerLeft, child: Container( width: 100, height: 11, color: color, ), ), subtitle: Container( color: color, width: 125, height: 11, ), trailing: Container( color: color, width: 50, height: 11, ), ), ); } } ================================================ FILE: lib/common/widgets/appbar/appbar.dart ================================================ import 'package:PiliPlus/pages/common/multi_select/base.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; class MultiSelectAppBarWidget extends StatelessWidget implements PreferredSizeWidget { final MultiSelectBase ctr; final bool? visible; final AppBar child; final List? actions; const MultiSelectAppBarWidget({ super.key, required this.ctr, this.visible, this.actions, required this.child, }); @override Widget build(BuildContext context) { if (visible ?? ctr.enableMultiSelect.value) { final style = TextButton.styleFrom(visualDensity: VisualDensity.compact); final colorScheme = ColorScheme.of(context); return AppBar( bottom: child.bottom, leading: IconButton( tooltip: '取消', onPressed: ctr.handleSelect, icon: const Icon(Icons.close_outlined), ), title: Obx(() => Text('已选: ${ctr.checkedCount}')), actions: [ TextButton( style: style, onPressed: () => ctr.handleSelect(checked: true), child: const Text('全选'), ), ...?actions, TextButton( style: style, onPressed: () { if (ctr.checkedCount == 0) { return; } ctr.onRemove(); }, child: Text( '移除', style: TextStyle(color: colorScheme.error), ), ), const SizedBox(width: 6), ], ); } return child; } @override Size get preferredSize => child.preferredSize; } ================================================ FILE: lib/common/widgets/avatars.dart ================================================ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/model_owner.dart'; import 'package:flutter/material.dart'; Widget avatars({ required ColorScheme colorScheme, required Iterable users, }) { const gap = 6.0; const size = 22.0; const padding = 0.8; const offset = size - gap; const imgSize = size - 2 * padding; if (users.length == 1) { return NetworkImgLayer( src: users.first.face, width: imgSize, height: imgSize, type: .avatar, ); } else { final decoration = BoxDecoration( shape: .circle, border: Border.all(color: colorScheme.surface), ); return SizedBox( height: size, width: offset * users.length + gap, child: Stack( clipBehavior: .none, children: users.indexed .map( (e) => Positioned( top: 0, bottom: 0, width: size, left: e.$1 * offset, child: DecoratedBox( decoration: decoration, child: Padding( padding: const .all(padding), child: NetworkImgLayer( src: e.$2.face, width: imgSize, height: imgSize, type: .avatar, ), ), ), ), ) .toList(), ), ); } } ================================================ FILE: lib/common/widgets/back_detector.dart ================================================ import 'package:flutter/gestures.dart' show kBackMouseButton; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show KeyDownEvent; class BackDetector extends StatelessWidget { const BackDetector({ super.key, required this.onBack, required this.child, }); final Widget child; final VoidCallback onBack; @override Widget build(BuildContext context) { return Focus( canRequestFocus: false, onKeyEvent: _onKeyEvent, child: Listener( behavior: .translucent, onPointerDown: _onPointerDown, child: child, ), ); } KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { if (event.logicalKey == .escape && event is KeyDownEvent) { onBack(); return .handled; } return .ignored; } void _onPointerDown(PointerDownEvent event) { if (event.buttons == kBackMouseButton) { onBack(); } } } ================================================ FILE: lib/common/widgets/badge.dart ================================================ import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:flutter/material.dart'; class PBadge extends StatelessWidget { final String? text; final bool isStack; final double? top; final double? right; final double? bottom; final double? left; final EdgeInsets? padding; final PBadgeType type; final PBadgeSize size; final double fontSize; final bool isBold; final double? textScaleFactor; const PBadge({ super.key, required this.text, this.top, this.right, this.bottom, this.left, this.type = PBadgeType.primary, this.size = PBadgeSize.medium, this.isStack = true, this.fontSize = 11, this.isBold = true, this.textScaleFactor, this.padding, }); @override Widget build(BuildContext context) { if (text.isNullOrEmpty) { return const SizedBox.shrink(); } ColorScheme theme = Theme.of(context).colorScheme; Color bgColor; Color color; Color borderColor = Colors.transparent; switch (type) { case PBadgeType.primary: bgColor = theme.primary; color = theme.onPrimary; case PBadgeType.secondary: bgColor = theme.secondaryContainer.withValues(alpha: 0.5); color = theme.onSecondaryContainer; case PBadgeType.gray: bgColor = Colors.black45; color = Colors.white; case PBadgeType.error: if (theme.isDark) { bgColor = theme.errorContainer; color = theme.onErrorContainer; } else { bgColor = theme.error; color = theme.onError; } case PBadgeType.line_primary: color = theme.primary; bgColor = Colors.transparent; borderColor = theme.primary; case PBadgeType.line_secondary: color = theme.secondary; bgColor = Colors.transparent; borderColor = theme.secondary; case PBadgeType.free: bgColor = theme.freeColor; color = Colors.white; case PBadgeType.shop: bgColor = theme.secondaryContainer.withValues(alpha: 0.5); color = theme.onSurfaceVariant; } late EdgeInsets paddingStyle = const EdgeInsets.symmetric( vertical: 2, horizontal: 3, ); BorderRadius br = size == PBadgeSize.small ? const BorderRadius.all(Radius.circular(3)) : const BorderRadius.all(Radius.circular(4)); Widget content = Container( padding: padding ?? paddingStyle, decoration: BoxDecoration( borderRadius: br, color: bgColor, border: Border.all(color: borderColor), ), child: Text( text!, textScaler: textScaleFactor != null ? TextScaler.linear(textScaleFactor!) : null, style: TextStyle( height: 1, fontSize: fontSize, color: color, fontWeight: isBold ? FontWeight.bold : null, ), strutStyle: StrutStyle( leading: 0, height: 1, fontSize: fontSize, fontWeight: isBold ? FontWeight.bold : null, ), ), ); if (isStack) { return Positioned( top: top, left: left, right: right, bottom: bottom, child: content, ); } else { return content; } } } ================================================ FILE: lib/common/widgets/button/icon_button.dart ================================================ import 'package:flutter/material.dart'; Widget iconButton({ BuildContext? context, String? tooltip, required Widget icon, required VoidCallback? onPressed, double size = 36, double? iconSize, Color? bgColor, Color? iconColor, }) { Color? backgroundColor = bgColor; Color? foregroundColor = iconColor; if (context != null) { final colorScheme = ColorScheme.of(context); backgroundColor = colorScheme.secondaryContainer; foregroundColor = colorScheme.onSecondaryContainer; } return SizedBox( width: size, height: size, child: IconButton( icon: icon, tooltip: tooltip, onPressed: onPressed, style: IconButton.styleFrom( padding: EdgeInsets.zero, iconSize: iconSize ?? size / 2, backgroundColor: backgroundColor, foregroundColor: foregroundColor, ), ), ); } ================================================ FILE: lib/common/widgets/button/more_btn.dart ================================================ import 'package:flutter/material.dart'; Widget moreTextButton({ String text = '查看更多', required VoidCallback onTap, EdgeInsets? padding, Color? color, }) { Widget child = Text.rich( style: TextStyle(color: color, height: 1), strutStyle: const StrutStyle(leading: 0, height: 1), TextSpan( children: [ TextSpan(text: text), WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( size: 22, color: color, Icons.keyboard_arrow_right, ), ), ], ), ); if (padding != null) { child = Padding(padding: padding, child: child); } return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: child, ); } ================================================ FILE: lib/common/widgets/button/toolbar_icon_button.dart ================================================ import 'package:flutter/material.dart'; class ToolbarIconButton extends StatelessWidget { final VoidCallback? onPressed; final Icon icon; final bool selected; final String? tooltip; const ToolbarIconButton({ super.key, this.onPressed, required this.icon, required this.selected, this.tooltip, }); @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return SizedBox( width: 36, height: 36, child: IconButton( tooltip: tooltip, onPressed: onPressed, icon: icon, highlightColor: theme.colorScheme.secondaryContainer, color: selected ? theme.colorScheme.onSecondaryContainer : theme.colorScheme.outline, style: ButtonStyle( padding: const WidgetStatePropertyAll(EdgeInsets.zero), backgroundColor: WidgetStatePropertyAll( selected ? theme.colorScheme.secondaryContainer : null, ), ), ), ); } } ================================================ FILE: lib/common/widgets/color_palette.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:flutter/material.dart'; class ColorPalette extends StatelessWidget { final ColorScheme colorScheme; final bool selected; final bool showBgColor; const ColorPalette({ super.key, required this.colorScheme, required this.selected, this.showBgColor = true, }); @override Widget build(BuildContext context) { final primary = colorScheme.primary; final tertiary = colorScheme.tertiary; final primaryContainer = colorScheme.primaryContainer; Widget child = ClipOval( child: Column( children: [ _coloredBox(primary), Expanded( child: Row( children: [ _coloredBox(tertiary), _coloredBox(primaryContainer), ], ), ), ], ), ); if (selected) { child = Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ child, Container( width: 23, height: 23, decoration: BoxDecoration( color: colorScheme.surfaceContainer, shape: BoxShape.circle, ), child: Icon( Icons.check_rounded, color: primary, size: 12, ), ), ], ); } if (showBgColor) { return Container( width: 50, height: 50, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: colorScheme.onInverseSurface, borderRadius: StyleString.mdRadius, ), child: child, ); } return child; } static Widget _coloredBox(Color color) => Expanded( child: ColoredBox( color: color, child: const SizedBox.expand(), ), ); } ================================================ FILE: lib/common/widgets/colored_box_transition.dart ================================================ import 'package:flutter/material.dart'; class ColoredBoxTransition extends AnimatedWidget { const ColoredBoxTransition({ super.key, required this.color, this.child, }) : super(listenable: color); final Animation color; final Widget? child; @override Widget build(BuildContext context) { return ColoredBox( color: color.value!, child: child, ); } } ================================================ FILE: lib/common/widgets/cropped_image.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; class CroppedImage extends LeafRenderObjectWidget { const CroppedImage({ super.key, required this.size, required this.image, required this.srcRect, required this.dstRect, required this.rrect, required this.imgPaint, required this.borderPaint, }); final Size size; final ui.Image image; final Rect srcRect; final Rect dstRect; final RRect rrect; final Paint imgPaint; final Paint borderPaint; @override RenderObject createRenderObject(BuildContext context) { return RenderCroppedImage( preferredSize: size, image: image, srcRect: srcRect, dstRect: dstRect, rrect: rrect, imgPaint: imgPaint, borderPaint: borderPaint, ); } @override void updateRenderObject( BuildContext context, RenderCroppedImage renderObject, ) { renderObject ..preferredSize = size ..image = image ..srcRect = srcRect ..dstRect = dstRect ..rrect = rrect ..imgPaint = imgPaint ..borderPaint = borderPaint; } } class RenderCroppedImage extends RenderBox { RenderCroppedImage({ required Size preferredSize, required ui.Image image, required Rect srcRect, required Rect dstRect, required RRect rrect, required Paint imgPaint, required Paint borderPaint, }) : _preferredSize = preferredSize, _image = image, _srcRect = srcRect, _dstRect = dstRect, _rrect = rrect, _imgPaint = imgPaint, _borderPaint = borderPaint; Size _preferredSize; Size get preferredSize => _preferredSize; set preferredSize(Size value) { if (_preferredSize == value) return; _preferredSize = value; markNeedsLayout(); } ui.Image _image; ui.Image get image => _image; set image(ui.Image value) { if (_image == value) return; _image = value; markNeedsPaint(); } Rect _srcRect; Rect get srcRect => _srcRect; set srcRect(Rect value) { if (_srcRect == value) return; _srcRect = value; markNeedsPaint(); } Rect _dstRect; Rect get dstRect => _dstRect; set dstRect(Rect value) { if (_dstRect == value) return; _dstRect = value; markNeedsPaint(); } RRect _rrect; RRect get rrect => _rrect; set rrect(RRect value) { if (_rrect == value) return; _rrect = value; markNeedsPaint(); } Paint _imgPaint; Paint get imgPaint => _imgPaint; set imgPaint(Paint value) { if (_imgPaint == value) return; _imgPaint = value; markNeedsPaint(); } Paint _borderPaint; Paint get borderPaint => _borderPaint; set borderPaint(Paint value) { if (_borderPaint == value) return; _borderPaint = value; markNeedsPaint(); } @override void performLayout() { size = constraints.constrain(_preferredSize); } @override void paint(PaintingContext context, Offset offset) { context.canvas ..drawImageRect(image, srcRect, dstRect, _imgPaint) ..drawRRect(rrect, _borderPaint); } @override bool get isRepaintBoundary => true; } ================================================ FILE: lib/common/widgets/custom_arc.dart ================================================ import 'dart:math' show pi; import 'package:flutter/widgets.dart'; class Arc extends LeafRenderObjectWidget { const Arc({ super.key, required this.size, required this.color, required this.progress, this.strokeWidth = 2, }); final double size; final Color color; final double progress; final double strokeWidth; @override RenderObject createRenderObject(BuildContext context) { return RenderArc( preferredSize: size, color: color, progress: progress, strokeWidth: strokeWidth, ); } @override void updateRenderObject( BuildContext context, RenderArc renderObject, ) { renderObject ..preferredSize = size ..color = color ..progress = progress ..strokeWidth = strokeWidth; } } class RenderArc extends RenderBox { RenderArc({ required double preferredSize, required Color color, required double progress, required double strokeWidth, }) : _preferredSize = preferredSize, _color = color, _progress = progress, _strokeWidth = strokeWidth; Color _color; Color get color => _color; set color(Color value) { if (_color == value) return; _color = value; markNeedsPaint(); } double _progress; double get progress => _progress; set progress(double value) { if (_progress == value) return; _progress = value; markNeedsPaint(); } double _strokeWidth; double get strokeWidth => _strokeWidth; set strokeWidth(double value) { if (_strokeWidth == value) return; _strokeWidth = value; markNeedsPaint(); } double _preferredSize; double get preferredSize => _preferredSize; set preferredSize(double value) { if (_preferredSize == value) return; _preferredSize = value; markNeedsLayout(); } @override void performLayout() { size = constraints.constrainDimensions(_preferredSize, _preferredSize); } @override void paint(PaintingContext context, Offset offset) { if (progress == 0) { return; } final paint = Paint() ..color = color ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; final radius = size.width / 2; final rect = Rect.fromCircle( center: Offset(radius, radius), radius: radius, ); const startAngle = -pi / 2; context.canvas.drawArc(rect, startAngle, progress * 2 * pi, false, paint); } @override bool get isRepaintBoundary => true; } ================================================ FILE: lib/common/widgets/custom_height_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult; class CustomHeightWidget extends SingleChildRenderObjectWidget { const CustomHeightWidget({ super.key, this.height, this.offset = .zero, required Widget super.child, }); final double? height; final Offset offset; @override RenderObject createRenderObject(BuildContext context) { return RenderCustomHeightWidget( height: height, offset: offset, ); } @override void updateRenderObject( BuildContext context, RenderCustomHeightWidget renderObject, ) { renderObject ..height = height ..offset = offset; } } class RenderCustomHeightWidget extends RenderProxyBox { RenderCustomHeightWidget({ double? height, required Offset offset, }) : _height = height, _offset = offset; double? _height; double? get height => _height; set height(double? value) { if (_height == value) return; _height = value; markNeedsLayout(); } Offset _offset; Offset get offset => _offset; set offset(Offset value) { if (_offset == value) return; _offset = value; markNeedsPaint(); } @override void performLayout() { if (height != null) { child!.layout(constraints.copyWith(maxHeight: .infinity)); size = constraints.constrainDimensions(constraints.maxWidth, height!); } else { child!.layout( constraints.copyWith(maxHeight: .infinity), parentUsesSize: true, ); size = constraints.constrainDimensions( constraints.maxWidth, child!.size.height, ); } } @override void paint(PaintingContext context, Offset offset) { context.paintChild(child!, offset + _offset); } @override bool hitTest(BoxHitTestResult result, {required Offset position}) { return result.addWithPaintOffset( offset: _offset, position: position, hitTest: (BoxHitTestResult result, Offset transformed) { assert(transformed == position - _offset); return child!.hitTest(result, position: transformed); }, ); } @override void applyPaintTransform(covariant RenderObject child, Matrix4 transform) { transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0); } } ================================================ FILE: lib/common/widgets/custom_icon.dart ================================================ // ignore_for_file: constant_identifier_names import 'package:flutter/widgets.dart'; class CustomIcons { static const IconData coin = _CustomIconData(0xe800); static const IconData dm_off = _CustomIconData(0xe801); static const IconData dm_on = _CustomIconData(0xe802); static const IconData dm_settings = _CustomIconData(0xe803); static const IconData dyn = _CustomIconData(0xe804); static const IconData fav = _CustomIconData(0xe805); static const IconData live_reserve = _CustomIconData(0xe806); static const IconData player_dm_tip_back = _CustomIconData(0xe807); static const IconData player_dm_tip_copy = _CustomIconData(0xe808); static const IconData player_dm_tip_like = _CustomIconData(0xe809); static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a); static const IconData player_dm_tip_recall = _CustomIconData(0xe80b); static const IconData share = _CustomIconData(0xe80c); static const IconData share_line = _CustomIconData(0xe80d); static const IconData share_node = _CustomIconData(0xe80e); static const IconData star_favorite_line = _CustomIconData(0xe80f); static const IconData star_favorite_solid = _CustomIconData(0xe810); static const IconData thumbs_down = _CustomIconData(0xe811); static const IconData thumbs_down_outline = _CustomIconData(0xe812); static const IconData thumbs_up = _CustomIconData(0xe813); static const IconData thumbs_up_fill = _CustomIconData(0xe814); static const IconData thumbs_up_line = _CustomIconData(0xe815); static const IconData thumbs_up_outline = _CustomIconData(0xe816); static const IconData topic_tag = _CustomIconData(0xe817); static const IconData watch_later = _CustomIconData(0xe818); } class _CustomIconData extends IconData { const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon'); } ================================================ FILE: lib/common/widgets/custom_toast.dart ================================================ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; class CustomToast extends StatelessWidget { const CustomToast({super.key, required this.msg}); final String msg; static double toastOpacity = Pref.defaultToastOp; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return Container( margin: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 30, ), padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer.withValues( alpha: toastOpacity, ), borderRadius: const BorderRadius.all(Radius.circular(20)), ), child: Text( msg, style: TextStyle( fontSize: 13, color: theme.colorScheme.onPrimaryContainer, ), ), ); } } class LoadingWidget extends StatelessWidget { const LoadingWidget({super.key, required this.msg}); ///loading msg final String msg; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; return Container( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), decoration: BoxDecoration( color: theme.dialogTheme.backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(15)), ), child: Column( spacing: 20, mainAxisSize: MainAxisSize.min, children: [ //loading animation CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(onSurfaceVariant), ), //msg Text(msg, style: TextStyle(color: onSurfaceVariant)), ], ), ); } } ================================================ FILE: lib/common/widgets/custom_tooltip.dart ================================================ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart' show ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin, MultiChildLayoutParentData; import 'package:flutter/widgets.dart'; class CustomTooltip extends StatefulWidget { const CustomTooltip({ super.key, required this.overlayWidget, required this.child, required this.indicator, }); final Widget child; final ValueGetter overlayWidget; final ValueGetter indicator; @override State createState() => _CustomTooltipState(); } class _CustomTooltipState extends State { final OverlayPortalController _overlayController = OverlayPortalController(); LongPressGestureRecognizer? _longPressRecognizer; LongPressGestureRecognizer get longPressRecognizer => _longPressRecognizer ??= LongPressGestureRecognizer() ..onLongPress = _scheduleShowTooltip; void _scheduleShowTooltip() { _overlayController.show(); } void _scheduleDismissTooltip() { _overlayController.hide(); } void _handlePointerDown(PointerDownEvent event) { assert(mounted); longPressRecognizer.addPointer(event); } Widget _buildCustomTooltipOverlay( BuildContext context, OverlayChildLayoutInfo layoutInfo, ) { final target = MatrixUtils.transformPoint( layoutInfo.childPaintTransform, layoutInfo.childSize.topCenter(Offset.zero), ); final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay( target: target, onDismiss: _scheduleDismissTooltip, overlayWidget: widget.overlayWidget, indicator: widget.indicator, ); return SelectionContainer.maybeOf(context) == null ? overlayChild : SelectionContainer.disabled(child: overlayChild); } @protected @override void dispose() { _longPressRecognizer ?..onLongPress = null ..dispose(); _longPressRecognizer = null; super.dispose(); } @protected @override Widget build(BuildContext context) { Widget result; if (PlatformUtils.isMobile) { result = Listener( onPointerDown: _handlePointerDown, behavior: HitTestBehavior.opaque, child: widget.child, ); } else { result = MouseRegion( cursor: MouseCursor.defer, onEnter: (_) => _scheduleShowTooltip(), onExit: (_) => _scheduleDismissTooltip(), child: widget.child, ); } return OverlayPortal.overlayChildLayoutBuilder( controller: _overlayController, overlayChildBuilder: _buildCustomTooltipOverlay, child: result, ); } } class _CustomTooltipOverlay extends StatelessWidget { const _CustomTooltipOverlay({ required this.target, required this.onDismiss, required this.overlayWidget, required this.indicator, }); final Offset target; final VoidCallback onDismiss; final ValueGetter overlayWidget; final ValueGetter indicator; @override Widget build(BuildContext context) { return _ToolTip( target: target, preferBelow: false, onTap: PlatformUtils.isMobile ? onDismiss : null, children: [ indicator(), overlayWidget(), ], ); } } class _ToolTip extends MultiChildRenderObjectWidget { const _ToolTip({ super.children, this.onTap, required this.target, required this.preferBelow, }); final VoidCallback? onTap; final Offset target; final bool preferBelow; @override RenderObject createRenderObject(BuildContext context) { return _RenderToolTip( onTap: onTap, target: target, preferBelow: preferBelow, ); } @override void updateRenderObject(BuildContext context, _RenderToolTip renderObject) { renderObject ..onTap = onTap ..target = target ..preferBelow = preferBelow; } } class _RenderToolTip extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { _RenderToolTip({ VoidCallback? onTap, required Offset target, required bool preferBelow, }) : _target = target, _preferBelow = preferBelow, _hitTestSelf = onTap != null { if (onTap != null) { _tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap; } } TapGestureRecognizer? _tapGestureRecognizer; set onTap(VoidCallback? value) { _tapGestureRecognizer?.onTap = value; } @override void dispose() { _tapGestureRecognizer ?..onTap = null ..dispose(); _tapGestureRecognizer = null; super.dispose(); } final bool _hitTestSelf; @override bool hitTestSelf(Offset position) => _hitTestSelf; @override void handleEvent(PointerEvent event, HitTestEntry entry) { if (event is PointerDownEvent) { _tapGestureRecognizer?.addPointer(event); } } Offset _target; Offset get target => _target; set target(Offset value) { if (_target == value) return; _target = value; markNeedsPaint(); } bool _preferBelow; bool get preferBelow => _preferBelow; set preferBelow(bool value) { if (_preferBelow == value) return; _preferBelow = value; markNeedsPaint(); } @override void setupParentData(RenderBox child) { if (child.parentData is! MultiChildLayoutParentData) { child.parentData = MultiChildLayoutParentData(); } } @override void performLayout() { size = constraints.constrain(constraints.biggest); final c = BoxConstraints.loose(size); RenderBox indicator = firstChild!..layout(c, parentUsesSize: true); RenderBox overlay = lastChild!..layout(c, parentUsesSize: true); final indicatorSize = indicator.size; final overlaySize = overlay.size; final indicatorParentData = indicator.parentData as MultiChildLayoutParentData; final overlayParentData = overlay.parentData as MultiChildLayoutParentData; Offset offset = positionDependentBox( size: size, childSize: overlaySize, target: target, preferBelow: preferBelow, ); offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1); overlayParentData.offset = offset; indicatorParentData.offset = Offset( target.dx - indicatorSize.width / 2, offset.dy + overlaySize.height - 1, ); } @override void paint(PaintingContext context, Offset offset) { defaultPaint(context, offset); } } class Triangle extends LeafRenderObjectWidget { const Triangle({ super.key, required this.color, required this.size, }); final Color color; final Size size; @override RenderObject createRenderObject(BuildContext context) { return RenderTriangle( color: color, preferredSize: size, ); } @override void updateRenderObject( BuildContext context, RenderTriangle renderObject, ) { renderObject ..color = color ..preferredSize = size; } } class RenderTriangle extends RenderBox { RenderTriangle({ required Color color, required Size preferredSize, }) : _color = color, _preferredSize = preferredSize; Color _color; Color get color => _color; set color(Color value) { if (_color == value) return; _color = value; markNeedsPaint(); } Size _preferredSize; set preferredSize(Size value) { if (_preferredSize == value) return; _preferredSize = value; markNeedsLayout(); } @override void performLayout() { size = constraints.constrain(_preferredSize); } @override void paint(PaintingContext context, Offset offset) { final size = this.size; final paint = Paint() ..color = color ..style = PaintingStyle.fill; final path = Path() ..moveTo(offset.dx, offset.dy) ..lineTo(offset.dx + size.width, offset.dy) ..lineTo(offset.dx + size.width / 2, size.height + offset.dy) ..close(); context.canvas.drawPath(path, paint); } } ================================================ FILE: lib/common/widgets/dialog/dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; Future showConfirmDialog({ required BuildContext context, required String title, Object? content, // @Deprecated('use `bool result = await showConfirmDialog()` instead') VoidCallback? onConfirm, }) async { assert(content is String? || content is Widget); return await showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: content is String ? Text(content) : content is Widget ? content : null, actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () { Get.back(result: true); onConfirm?.call(); }, child: const Text('确认'), ), ], ), ) ?? false; } void showPgcFollowDialog({ required BuildContext context, required String type, required int followStatus, required ValueChanged onUpdateStatus, }) { Widget statusItem({ required bool enabled, required String text, required VoidCallback onTap, }) { return ListTile( dense: true, enabled: enabled, title: Padding( padding: const EdgeInsets.only(left: 10), child: Text( '标记为 $text', style: const TextStyle(fontSize: 14), ), ), trailing: !enabled ? const Icon(size: 22, Icons.check) : null, onTap: onTap, ); } showDialog( context: context, builder: (context) => AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: Column( mainAxisSize: MainAxisSize.min, children: [ ...const [ (followStatus: 3, title: '看过'), (followStatus: 2, title: '在看'), (followStatus: 1, title: '想看'), ].map( (item) => statusItem( enabled: followStatus != item.followStatus, text: item.title, onTap: () { Get.back(); onUpdateStatus(item.followStatus); }, ), ), ListTile( dense: true, title: Padding( padding: const EdgeInsets.only(left: 10), child: Text( '取消$type', style: const TextStyle(fontSize: 14), ), ), onTap: () { Get.back(); onUpdateStatus(-1); }, ), ], ), ), ); } ================================================ FILE: lib/common/widgets/dialog/export_import.dart ================================================ import 'dart:async' show FutureOr; import 'dart:convert' show utf8, jsonDecode; import 'dart:io' show File; import 'package:PiliPlus/common/constants.dart' show StyleString; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show Clipboard; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; import 'package:intl/intl.dart' show DateFormat; import 'package:re_highlight/languages/json.dart'; import 'package:re_highlight/re_highlight.dart'; import 'package:re_highlight/styles/base16/github.dart'; import 'package:re_highlight/styles/github-dark.dart'; void exportToClipBoard({ required ValueGetter onExport, }) { Utils.copyText(onExport()); } void exportToLocalFile({ required ValueGetter onExport, required ValueGetter localFileName, }) { final res = utf8.encode(onExport()); Utils.saveBytes2File( name: 'piliplus_${localFileName()}_' '${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json', bytes: res, allowedExtensions: const ['json'], ); } Future importFromClipBoard( BuildContext context, { required String title, required ValueGetter onExport, required FutureOr Function(T json) onImport, bool showConfirmDialog = true, }) async { final data = await Clipboard.getData('text/plain'); if (data?.text?.isNotEmpty != true) { SmartDialog.showToast('剪贴板无数据'); return; } if (!context.mounted) return; final text = data!.text!; late final T json; late final String formatText; try { json = jsonDecode(text); formatText = Utils.jsonEncoder.convert(json); } catch (e) { SmartDialog.showToast('解析json失败:$e'); return; } bool? executeImport; if (showConfirmDialog) { final highlight = Highlight()..registerLanguage('json', langJson); final result = highlight.highlight( code: formatText, language: 'json', ); late TextSpanRenderer renderer; bool? isDarkMode; executeImport = await showDialog( context: context, builder: (context) { final isDark = context.isDarkMode; if (isDark != isDarkMode) { isDarkMode = isDark; renderer = TextSpanRenderer( const TextStyle(), isDark ? githubDarkTheme : githubTheme, ); result.render(renderer); } return AlertDialog( title: Text('是否导入如下$title?'), content: SingleChildScrollView( child: Text.rich(renderer.span!), ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () => Get.back(result: true), child: const Text('确定'), ), ], ); }, ); } else { executeImport = true; } if (executeImport ?? false) { try { await onImport(json); SmartDialog.showToast('导入成功'); } catch (e) { SmartDialog.showToast('导入失败:$e'); } } } Future importFromLocalFile({ required FutureOr Function(T json) onImport, }) async { final result = await FilePicker.pickFiles(); if (result != null) { final path = result.files.first.path; if (path != null) { final data = await File(path).readAsString(); late final T json; try { json = jsonDecode(data); } catch (e) { SmartDialog.showToast('解析json失败:$e'); return; } try { await onImport(json); SmartDialog.showToast('导入成功'); } catch (e) { SmartDialog.showToast('导入失败:$e'); } } } } void importFromInput( BuildContext context, { required String title, required FutureOr Function(T json) onImport, }) { final key = GlobalKey>(); late T json; String? forceErrorText; showDialog( context: context, builder: (context) => AlertDialog( title: Text('输入$title'), constraints: StyleString.dialogFixedConstraints, content: TextFormField( key: key, minLines: 4, maxLines: 12, autofocus: true, decoration: const InputDecoration( border: OutlineInputBorder(), errorMaxLines: 3, ), validator: (value) { if (forceErrorText != null) return forceErrorText; try { json = jsonDecode(value!) as T; return null; } catch (e) { if (e is FormatException) {} return '解析json失败:$e'; } }, ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () async { if (key.currentState?.validate() == true) { try { await onImport(json); Get.back(); SmartDialog.showToast('导入成功'); return; } catch (e) { forceErrorText = '导入失败:$e'; } key.currentState?.validate(); forceErrorText = null; } }, child: const Text('确定'), ), ], ), ); } Future showImportExportDialog( BuildContext context, { required String title, required ValueGetter onExport, required FutureOr Function(T json) onImport, required ValueGetter localFileName, }) => showDialog( context: context, builder: (context) { const style = TextStyle(fontSize: 15); return SimpleDialog( clipBehavior: Clip.hardEdge, title: Text('导入/导出$title'), children: [ ListTile( dense: true, title: const Text('导出至剪贴板', style: style), onTap: () { Get.back(); exportToClipBoard(onExport: onExport); }, ), ListTile( dense: true, title: const Text('导出文件至本地', style: style), onTap: () { Get.back(); exportToLocalFile(onExport: onExport, localFileName: localFileName); }, ), Divider( height: 1, color: ColorScheme.of(context).outline.withValues(alpha: 0.1), ), ListTile( dense: true, title: const Text('输入', style: style), onTap: () { Get.back(); importFromInput(context, title: title, onImport: onImport); }, ), ListTile( dense: true, title: const Text('从剪贴板导入', style: style), onTap: () { Get.back(); importFromClipBoard( context, title: title, onExport: onExport, onImport: onImport, ); }, ), ListTile( dense: true, title: const Text('从本地文件导入', style: style), onTap: () { Get.back(); importFromLocalFile(onImport: onImport); }, ), ], ); }, ); ================================================ FILE: lib/common/widgets/dialog/report.dart ================================================ import 'package:PiliPlus/common/widgets/radio_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; Future autoWrapReportDialog( BuildContext context, Map> options, Future Function(int reasonType, String? reasonDesc, bool banUid) onSuccess, { bool ban = true, }) { int? reasonType; String? reasonDesc; bool banUid = false; late final key = GlobalKey>(); return showDialog( context: context, builder: (context) => AlertDialog( title: const Text('举报'), titlePadding: const .only(left: 22, top: 16, right: 22), contentPadding: const .symmetric(vertical: 5), actionsPadding: const .only(left: 16, right: 16, bottom: 10), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: SingleChildScrollView( child: AnimatedSize( duration: const Duration(milliseconds: 200), child: Builder( builder: (context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: .only(left: 22, right: 22, bottom: 5), child: Text('请选择举报的理由:'), ), RadioGroup( onChanged: (value) { reasonType = value; (context as Element).markNeedsBuild(); }, groupValue: reasonType, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: options.entries.map((entry) { return WrapRadioOptionsGroup( groupTitle: entry.key, options: entry.value, ); }).toList(), ), ), if (reasonType == 0) Padding( padding: const .only(left: 22, top: 5, right: 22), child: TextFormField( key: key, autofocus: true, minLines: 2, maxLines: 4, initialValue: reasonDesc, decoration: const InputDecoration( labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息', border: OutlineInputBorder(), contentPadding: .all(10), labelStyle: TextStyle(fontSize: 14), floatingLabelStyle: TextStyle(fontSize: 14), ), onChanged: (value) => reasonDesc = value, validator: (value) => value.isNullOrEmpty ? '理由不能为空' : null, ), ), ], ), ), ), ), ), if (ban) Padding( padding: const EdgeInsets.only(left: 14, top: 6), child: CheckBoxText( text: '拉黑该用户', onChanged: (value) => banUid = value, ), ), ], ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle(color: ColorScheme.of(context).outline), ), ), TextButton( onPressed: () async { if (reasonType == null || (reasonType == 0 && key.currentState?.validate() != true)) { return; } SmartDialog.showLoading(); try { final res = await onSuccess(reasonType!, reasonDesc, banUid); SmartDialog.dismiss(); if (res.isSuccess) { Get.back(); SmartDialog.showToast('举报成功'); } else { res.toast(); } } catch (e, s) { SmartDialog.dismiss(); SmartDialog.showToast('提交失败:$e'); Utils.reportError(e, s); } }, child: const Text('确定'), ), ], ), ); } class CheckBoxText extends StatefulWidget { final String text; final ValueChanged onChanged; final bool selected; const CheckBoxText({ super.key, required this.text, required this.onChanged, this.selected = false, }); @override State createState() => _CheckBoxTextState(); } class _CheckBoxTextState extends State { late bool _selected; @override void initState() { super.initState(); _selected = widget.selected; } @override Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); return InkWell( onTap: () { setState(() { _selected = !_selected; widget.onChanged(_selected); }); }, child: Padding( padding: const EdgeInsets.all(4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( size: 22, _selected ? Icons.check_box_outlined : Icons.check_box_outline_blank, color: _selected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), Text( ' ${widget.text}', style: TextStyle(color: _selected ? colorScheme.primary : null), ), ], ), ), ); } } abstract final class ReportOptions { // from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js static Map> get commentReport => const { '违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'}, '谣言类不实信息': {19: '涉政谣言', 22: '虚假不实信息', 20: '涉社会事件谣言'}, '侵犯个人权益': {7: '人身攻击', 15: '侵犯隐私'}, '有害社区环境': { 1: '垃圾广告', 4: '引战', 5: '剧透', 3: '刷屏', 8: '视频不相关', 18: '违规抽奖', 17: '青少年不良信息', }, '其他': {0: '其他'}, }; static Map> get dynamicReport => const { '': { 4: '垃圾广告', 8: '引战', 1: '色情', 5: '人身攻击', 3: '违法信息', 9: '涉政谣言', 10: '涉社会事件谣言', 12: '虚假不实信息', 13: '违法信息外链', 0: '其他', }, }; static Map> get danmakuReport => const { '': { 1: '违法违禁', 2: '色情低俗', 3: '赌博诈骗', 4: '人身攻击', 5: '侵犯隐私', 6: '垃圾广告', 7: '引战', 8: '剧透', 9: '恶意刷屏', 10: '视频无关', 12: '青少年不良信息', 13: '违法信息外链', 0: '其它', // 11 }, }; static Map> get liveDanmakuReport => const { '': { 1: '违法违规', 2: '低俗色情', 3: '垃圾广告', 4: '辱骂引战', 5: '政治敏感', 6: '青少年不良信息', 7: '其他', // avoid show form }, }; static Map> get imMsgReport => const { '': { 1: '色情低俗', 2: '政治敏感', 3: '违法有害', 4: '广告骚扰', 5: '人身攻击', 6: '诈骗', 0: '其他问题', }, }; } ================================================ FILE: lib/common/widgets/dialog/report_member.dart ================================================ import 'package:PiliPlus/http/member.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; const _reason = ['头像违规', '昵称违规', '签名违规']; const _reasonV2 = ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗', '违规引流外链']; Future showMemberReportDialog( BuildContext context, { required Object? name, required Object mid, }) { final Set reason = {}; int? reasonV2; return showDialog( context: context, builder: (context) { final theme = Theme.of(context); return AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 16), titleTextStyle: theme.textTheme.bodyMedium, title: Column( spacing: 4, crossAxisAlignment: .start, children: [ Text( '举报: $name', style: const TextStyle(fontSize: 18), ), Text('uid: $mid'), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: .min, crossAxisAlignment: .start, children: [ const Padding( padding: .only(left: 18), child: Text('举报内容(必选,可多选)'), ), ...List.generate( 3, (index) => Builder( builder: (context) { final checked = reason.contains(index + 1); return ListTile( dense: true, minTileHeight: 40, onTap: () { if (!checked) { reason.add(index + 1); } else { reason.remove(index + 1); } (context as Element).markNeedsBuild(); }, title: Row( spacing: 8, children: [ checked ? Icon( size: 22, Icons.check_box, color: theme.colorScheme.primary, ) : Icon( size: 22, Icons.check_box_outline_blank, color: theme.colorScheme.onSurfaceVariant, ), Expanded( child: Text( _reason[index], style: const TextStyle(fontSize: 14), ), ), ], ), ); }, ), ), const Padding( padding: .only(left: 18), child: Text('举报理由(单选,非必选)'), ), Builder( builder: (context) => Column( crossAxisAlignment: .start, children: List.generate( _reasonV2.length, (index) { final checked = index == reasonV2; return ListTile( dense: true, minTileHeight: 40, onTap: () { if (checked) { reasonV2 = null; } else { reasonV2 = index; } (context as Element).markNeedsBuild(); }, title: Row( spacing: 8, children: [ checked ? Icon( size: 22, Icons.radio_button_checked, color: theme.colorScheme.primary, ) : Icon( size: 22, Icons.radio_button_off, color: theme.colorScheme.onSurfaceVariant, ), Expanded( child: Text( _reasonV2[index], style: const TextStyle(fontSize: 14), ), ), ], ), ); }, ), ), ), ], ), ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle(color: theme.colorScheme.outline), ), ), TextButton( onPressed: () { if (reason.isEmpty) { SmartDialog.showToast('至少选择一项作为举报内容'); } else { Get.back(); MemberHttp.reportMember( mid, reason: reason.join(','), reasonV2: reasonV2 != null ? reasonV2! + 1 : null, ); } }, child: const Text('确定'), ), ], ); }, ); } ================================================ FILE: lib/common/widgets/disabled_icon.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class DisabledIcon extends SingleChildRenderObjectWidget { const DisabledIcon({ super.key, required Widget super.child, this.disable = false, this.color, this.iconSize, this.lineLengthScale = 0.9, this.strokeCap = .butt, }); final bool disable; final Color? color; final double? iconSize; final StrokeCap strokeCap; final double lineLengthScale; Icon? get _icon => child is Icon ? child as Icon : null; @override RenderObject createRenderObject(BuildContext context) { late final iconTheme = IconTheme.of(context); final icon = _icon; return RenderMaskedIcon( disable: disable, iconSize: iconSize ?? icon?.size ?? iconTheme.size ?? 24.0, color: color ?? icon?.color ?? iconTheme.color!, strokeCap: strokeCap, lineLengthScale: lineLengthScale, ); } @override void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) { late final iconTheme = IconTheme.of(context); final icon = _icon; renderObject ..disable = disable ..iconSize = iconSize ?? icon?.size ?? iconTheme.size ?? 24.0 ..color = color ?? icon?.color ?? iconTheme.color! ..strokeCap = strokeCap ..lineLengthScale = lineLengthScale; } } class RenderMaskedIcon extends RenderProxyBox { RenderMaskedIcon({ required bool disable, required double iconSize, required Color color, required StrokeCap strokeCap, required double lineLengthScale, }) : _disable = disable, _iconSize = iconSize, _color = color, _strokeCap = strokeCap, _lineLengthScale = lineLengthScale; bool _disable; bool get disable => _disable; set disable(bool value) { if (_disable == value) return; _disable = value; markNeedsPaint(); } double _iconSize; double get iconSize => _iconSize; set iconSize(double value) { if (_iconSize == value) return; _iconSize = value; markNeedsPaint(); } Color _color; Color get color => _color; set color(Color value) { if (_color == value) return; _color = value; markNeedsPaint(); } StrokeCap _strokeCap; StrokeCap get strokeCap => _strokeCap; set strokeCap(StrokeCap value) { if (_strokeCap == value) return; _strokeCap = value; markNeedsPaint(); } double _lineLengthScale; double get lineLengthScale => _lineLengthScale; set lineLengthScale(double value) { if (_lineLengthScale == value) return; _lineLengthScale = value; markNeedsPaint(); } @override void paint(PaintingContext context, Offset offset) { if (!disable) { return super.paint(context, offset); } final canvas = context.canvas; var rectOffset = offset; Size size = this.size; final exceedWidth = size.width > _iconSize; final exceedHeight = size.height > _iconSize; if (exceedWidth || exceedHeight) { final dx = exceedWidth ? (size.width - _iconSize) / 2.0 : 0.0; final dy = exceedHeight ? (size.height - _iconSize) / 2.0 : 0.0; size = Size.square(_iconSize); rectOffset += Offset(dx, dy); } else if (size.width < _iconSize && size.height < _iconSize) { size = Size.square(_iconSize); } final strokeWidth = size.width / 12; var rect = rectOffset & size; final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4 final path = Path.combine( PathOperation.union, Path() // bottom ..moveTo(rect.left, rect.bottom) ..lineTo(rect.left, rect.top + sqrt2Width) ..lineTo(rect.right - sqrt2Width, rect.bottom) ..close(), Path() // top ..moveTo(rect.right, rect.top) ..lineTo(rect.right, rect.bottom - sqrt2Width) ..lineTo(rect.left + sqrt2Width, rect.top), ); canvas ..save() ..clipPath(path, doAntiAlias: false); super.paint(context, offset); canvas.restore(); final linePaint = Paint() ..color = color ..strokeWidth = strokeWidth ..strokeCap = strokeCap; final strokeOffset = strokeWidth * sqrt1_2 / 2; rect = rect .translate(-strokeOffset, strokeOffset) .deflate(size.width * lineLengthScale); canvas.drawLine( rect.topLeft, rect.bottomRight, linePaint, ); } } ================================================ FILE: lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:PiliPlus/common/widgets/custom_height_widget.dart'; import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart'; import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart'; import 'package:PiliPlus/common/widgets/only_layout_widget.dart' show LayoutCallback; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide SliverPersistentHeader, SliverPersistentHeaderDelegate; import 'package:flutter/rendering.dart' show RenderOpacity, OpacityLayer; import 'package:flutter/services.dart'; /// ref [SliverAppBar] class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate({ required this.leading, required this.automaticallyImplyLeading, required this.title, required this.actions, required this.automaticallyImplyActions, required this.flexibleSpace, required this.bottom, required this.elevation, required this.scrolledUnderElevation, required this.shadowColor, required this.surfaceTintColor, required this.forceElevated, required this.backgroundColor, required this.foregroundColor, required this.iconTheme, required this.actionsIconTheme, required this.primary, required this.centerTitle, required this.excludeHeaderSemantics, required this.titleSpacing, required this.collapsedHeight, required this.topPadding, required this.shape, required this.toolbarHeight, required this.leadingWidth, required this.toolbarTextStyle, required this.titleTextStyle, required this.systemOverlayStyle, required this.forceMaterialTransparency, required this.useDefaultSemanticsOrder, required this.clipBehavior, required this.actionsPadding, }) : assert(primary || topPadding == 0.0), _bottomHeight = bottom?.preferredSize.height ?? 0.0; final Widget? leading; final bool automaticallyImplyLeading; final Widget title; final List? actions; final bool automaticallyImplyActions; final Widget flexibleSpace; final PreferredSizeWidget? bottom; final double? elevation; final double? scrolledUnderElevation; final Color? shadowColor; final Color? surfaceTintColor; final bool forceElevated; final Color? backgroundColor; final Color? foregroundColor; final IconThemeData? iconTheme; final IconThemeData? actionsIconTheme; final bool primary; final bool? centerTitle; final bool excludeHeaderSemantics; final double? titleSpacing; final double collapsedHeight; final double topPadding; final ShapeBorder? shape; final double? toolbarHeight; final double? leadingWidth; final TextStyle? toolbarTextStyle; final TextStyle? titleTextStyle; final SystemUiOverlayStyle? systemOverlayStyle; final double _bottomHeight; final bool forceMaterialTransparency; final bool useDefaultSemanticsOrder; final Clip? clipBehavior; final EdgeInsetsGeometry? actionsPadding; @override double get minExtent => collapsedHeight; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent, double? maxExtent, ) { maxExtent ??= double.infinity; final bool isScrolledUnder = overlapsContent || forceElevated || (shrinkOffset > maxExtent - minExtent); final effectiveTitle = AnimatedOpacity( opacity: isScrolledUnder ? 1 : 0, duration: const Duration(milliseconds: 500), curve: const Cubic(0.2, 0.0, 0.0, 1.0), child: title, ); return FlexibleSpaceBar.createSettings( minExtent: minExtent, maxExtent: maxExtent, currentExtent: math.max(minExtent, maxExtent - shrinkOffset), isScrolledUnder: isScrolledUnder, hasLeading: leading != null || automaticallyImplyLeading, child: AppBar( clipBehavior: clipBehavior, leading: leading, automaticallyImplyLeading: automaticallyImplyLeading, title: effectiveTitle, actions: actions, automaticallyImplyActions: automaticallyImplyActions, flexibleSpace: IgnorePointer( ignoring: isScrolledUnder, child: DynamicFlexibleSpaceBar(background: flexibleSpace), ), bottom: bottom, elevation: isScrolledUnder ? elevation : 0.0, scrolledUnderElevation: scrolledUnderElevation, shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconTheme: iconTheme, actionsIconTheme: actionsIconTheme, primary: primary, centerTitle: centerTitle, excludeHeaderSemantics: excludeHeaderSemantics, titleSpacing: titleSpacing, shape: shape, toolbarHeight: toolbarHeight, leadingWidth: leadingWidth, toolbarTextStyle: toolbarTextStyle, titleTextStyle: titleTextStyle, systemOverlayStyle: systemOverlayStyle, forceMaterialTransparency: forceMaterialTransparency, useDefaultSemanticsOrder: useDefaultSemanticsOrder, actionsPadding: actionsPadding, ), ); } @override bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) { return leading != oldDelegate.leading || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading || title != oldDelegate.title || actions != oldDelegate.actions || automaticallyImplyActions != oldDelegate.automaticallyImplyActions || flexibleSpace != oldDelegate.flexibleSpace || bottom != oldDelegate.bottom || _bottomHeight != oldDelegate._bottomHeight || elevation != oldDelegate.elevation || shadowColor != oldDelegate.shadowColor || backgroundColor != oldDelegate.backgroundColor || foregroundColor != oldDelegate.foregroundColor || iconTheme != oldDelegate.iconTheme || actionsIconTheme != oldDelegate.actionsIconTheme || primary != oldDelegate.primary || centerTitle != oldDelegate.centerTitle || titleSpacing != oldDelegate.titleSpacing || topPadding != oldDelegate.topPadding || forceElevated != oldDelegate.forceElevated || toolbarHeight != oldDelegate.toolbarHeight || leadingWidth != oldDelegate.leadingWidth || toolbarTextStyle != oldDelegate.toolbarTextStyle || titleTextStyle != oldDelegate.titleTextStyle || systemOverlayStyle != oldDelegate.systemOverlayStyle || forceMaterialTransparency != oldDelegate.forceMaterialTransparency || useDefaultSemanticsOrder != oldDelegate.useDefaultSemanticsOrder || actionsPadding != oldDelegate.actionsPadding; } @override String toString() { return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; } } class DynamicSliverAppBar extends StatelessWidget { const DynamicSliverAppBar.medium({ super.key, this.leading, this.automaticallyImplyLeading = true, required this.title, this.actions, this.automaticallyImplyActions = true, required this.flexibleSpace, this.bottom, this.elevation, this.scrolledUnderElevation, this.shadowColor, this.surfaceTintColor, this.forceElevated = false, this.backgroundColor, this.foregroundColor, this.iconTheme, this.actionsIconTheme, this.primary = true, this.centerTitle, this.excludeHeaderSemantics = false, this.titleSpacing, this.shape, this.leadingWidth, this.toolbarTextStyle, this.titleTextStyle, this.systemOverlayStyle, this.forceMaterialTransparency = false, this.useDefaultSemanticsOrder = true, this.clipBehavior, this.actionsPadding, this.onPerformLayout, }); final LayoutCallback? onPerformLayout; final Widget? leading; final bool automaticallyImplyLeading; final Widget title; final List? actions; final bool automaticallyImplyActions; final Widget flexibleSpace; final PreferredSizeWidget? bottom; final double? elevation; final double? scrolledUnderElevation; final Color? shadowColor; final Color? surfaceTintColor; final bool forceElevated; final Color? backgroundColor; final Color? foregroundColor; final IconThemeData? iconTheme; final IconThemeData? actionsIconTheme; final bool primary; final bool? centerTitle; final bool excludeHeaderSemantics; final double? titleSpacing; final ShapeBorder? shape; final double? leadingWidth; final TextStyle? toolbarTextStyle; final TextStyle? titleTextStyle; final SystemUiOverlayStyle? systemOverlayStyle; final bool forceMaterialTransparency; final bool useDefaultSemanticsOrder; final Clip? clipBehavior; final EdgeInsetsGeometry? actionsPadding; @override Widget build(BuildContext context) { final double bottomHeight = bottom?.preferredSize.height ?? 0.0; final double topPadding = primary ? MediaQuery.viewPaddingOf(context).top : 0.0; final double effectiveCollapsedHeight = topPadding + kToolbarHeight + bottomHeight + 1; return SliverPinnedHeader( onPerformLayout: onPerformLayout, delegate: _SliverAppBarDelegate( leading: leading, automaticallyImplyLeading: automaticallyImplyLeading, title: title, actions: actions, automaticallyImplyActions: automaticallyImplyActions, flexibleSpace: flexibleSpace, bottom: bottom, elevation: elevation, scrolledUnderElevation: scrolledUnderElevation, shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, forceElevated: forceElevated, backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconTheme: iconTheme, actionsIconTheme: actionsIconTheme, primary: primary, centerTitle: centerTitle, excludeHeaderSemantics: excludeHeaderSemantics, titleSpacing: titleSpacing, collapsedHeight: effectiveCollapsedHeight, topPadding: topPadding, shape: shape, toolbarHeight: kToolbarHeight, leadingWidth: leadingWidth, toolbarTextStyle: toolbarTextStyle, titleTextStyle: titleTextStyle, systemOverlayStyle: systemOverlayStyle, forceMaterialTransparency: forceMaterialTransparency, useDefaultSemanticsOrder: useDefaultSemanticsOrder, clipBehavior: clipBehavior, actionsPadding: actionsPadding, ), ); } } /// ref [FlexibleSpaceBar] class DynamicFlexibleSpaceBar extends StatelessWidget { const DynamicFlexibleSpaceBar({ super.key, required this.background, this.collapseMode = CollapseMode.parallax, }); final Widget background; final CollapseMode collapseMode; static double _getCollapsePadding( CollapseMode collapseMode, double t, FlexibleSpaceBarSettings settings, ) { switch (collapseMode) { case CollapseMode.pin: return -(settings.maxExtent - settings.currentExtent); case CollapseMode.none: return 0.0; case CollapseMode.parallax: final double deltaExtent = settings.maxExtent - settings.minExtent; return -Tween(begin: 0.0, end: deltaExtent / 4.0).transform(t); } } @override Widget build(BuildContext context) { final FlexibleSpaceBarSettings settings = context .dependOnInheritedWidgetOfExactType()!; double? height; final double opacity; final double topPadding; if (settings.maxExtent == .infinity) { opacity = 1.0; topPadding = 0.0; } else { height = settings.maxExtent; final double deltaExtent = settings.maxExtent - settings.minExtent; // 0.0 -> Expanded // 1.0 -> Collapsed to toolbar final double t = clampDouble( 1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent, 0.0, 1.0, ); final double fadeStart = math.max( 0.0, 1.0 - kToolbarHeight / deltaExtent, ); const fadeEnd = 1.0; assert(fadeStart <= fadeEnd); // If the min and max extent are the same, the app bar cannot collapse // and the content should be visible, so opacity = 1. opacity = settings.maxExtent == settings.minExtent ? 1.0 : 1.0 - Interval(fadeStart, fadeEnd).transform(t); topPadding = _getCollapsePadding(collapseMode, t, settings); } return ClipRect( child: CustomHeightWidget( height: height, offset: Offset(0.0, topPadding), child: _FlexibleSpaceHeaderOpacity( // IOS is relying on this semantics node to correctly traverse // through the app bar when it is collapsed. alwaysIncludeSemantics: true, opacity: opacity, child: background, ), ), ); } } /// [_FlexibleSpaceHeaderOpacity] class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget { const _FlexibleSpaceHeaderOpacity({ required this.opacity, required super.child, required this.alwaysIncludeSemantics, }); final double opacity; final bool alwaysIncludeSemantics; @override RenderObject createRenderObject(BuildContext context) { return _RenderFlexibleSpaceHeaderOpacity( opacity: opacity, alwaysIncludeSemantics: alwaysIncludeSemantics, ); } @override void updateRenderObject( BuildContext context, covariant _RenderFlexibleSpaceHeaderOpacity renderObject, ) { renderObject ..alwaysIncludeSemantics = alwaysIncludeSemantics ..opacity = opacity; } } class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity { _RenderFlexibleSpaceHeaderOpacity({ super.opacity, super.alwaysIncludeSemantics, }); @override bool get isRepaintBoundary => false; @override void paint(PaintingContext context, Offset offset) { if (child == null) { return; } if ((opacity * 255).roundToDouble() <= 0) { layer = null; return; } assert(needsCompositing); layer = context.pushOpacity( offset, (opacity * 255).round(), super.paint, oldLayer: layer as OpacityLayer?, ); assert(() { layer!.debugCreator = debugCreator; return true; }()); } } ================================================ FILE: lib/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart'; import 'package:PiliPlus/common/widgets/only_layout_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart' hide LayoutCallback; import 'package:flutter/widgets.dart' hide SliverPersistentHeader, SliverPersistentHeaderDelegate; /// ref [SliverPersistentHeader] Rect? _trim( Rect? original, { double top = -double.infinity, double right = double.infinity, double bottom = double.infinity, double left = -double.infinity, }) => original?.intersect(Rect.fromLTRB(left, top, right, bottom)); abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin, RenderSliverHelpers { RenderSliverPersistentHeader({RenderBox? child}) { this.child = child; } SliverPersistentHeaderElement? element; double get minExtent => (element!.widget as SliverPinnedHeader).delegate.minExtent; bool _needsUpdateChild = true; double get lastShrinkOffset => _lastShrinkOffset; double _lastShrinkOffset = 0.0; bool get lastOverlapsContent => _lastOverlapsContent; bool _lastOverlapsContent = false; @protected void updateChild( double shrinkOffset, bool overlapsContent, double? maxExtent, ) { assert(element != null); element!.build(shrinkOffset, overlapsContent, maxExtent); } @override void markNeedsLayout() { _needsUpdateChild = true; super.markNeedsLayout(); } @protected void updateChildIfNeeded( double scrollOffset, double? maxExtent, { bool overlapsContent = false, }) { final double shrinkOffset = maxExtent == null ? scrollOffset : math.min(scrollOffset, maxExtent); if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) { invokeLayoutCallback((SliverConstraints constraints) { assert(constraints == this.constraints); updateChild(shrinkOffset, overlapsContent, maxExtent); }); _lastShrinkOffset = shrinkOffset; _lastOverlapsContent = overlapsContent; _needsUpdateChild = false; } } @override double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child); @override bool hitTestChildren( SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition, }) { assert(geometry!.hitTestExtent > 0.0); if (child != null) { return hitTestBoxChild( BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition, ); } return false; } @override void applyPaintTransform(RenderObject child, Matrix4 transform) { assert(child == this.child); applyPaintTransformForBoxChild(child as RenderBox, transform); } void triggerRebuild() { markNeedsLayout(); } } class SliverPinnedHeader extends RenderObjectWidget { const SliverPinnedHeader({ super.key, required this.delegate, this.onPerformLayout, }); final SliverPersistentHeaderDelegate delegate; final LayoutCallback? onPerformLayout; @override SliverPersistentHeaderElement createElement() => SliverPersistentHeaderElement(this); @override RenderSliverPinnedHeader createRenderObject(BuildContext context) { return RenderSliverPinnedHeader(onPerformLayout: onPerformLayout); } @override void updateRenderObject( BuildContext context, RenderSliverPinnedHeader renderObject, ) { renderObject.onPerformLayout = onPerformLayout; } } class RenderSliverPinnedHeader extends RenderSliverPersistentHeader { RenderSliverPinnedHeader({ super.child, this.onPerformLayout, }); LayoutCallback? onPerformLayout; ({double crossAxisExtent, double maxExtent})? _maxExtent; double? get maxExtent => _maxExtent?.maxExtent; void _rawLayout() { child!.layout(constraints.asBoxConstraints(), parentUsesSize: true); _maxExtent = ( crossAxisExtent: constraints.crossAxisExtent, maxExtent: child!.size.height, ); onPerformLayout?.call(child!.size); } void _layout() { final double shrinkOffset = math.min( constraints.scrollOffset, _maxExtent!.maxExtent, ); child!.layout( constraints.asBoxConstraints( maxExtent: math.max(minExtent, _maxExtent!.maxExtent - shrinkOffset), ), parentUsesSize: true, ); } @override void performLayout() { final constraints = this.constraints; final bool overlapsContent = constraints.overlap > 0.0; if (_maxExtent == null) { updateChildIfNeeded( constraints.scrollOffset, _maxExtent?.maxExtent, overlapsContent: overlapsContent, ); _rawLayout(); } else { if (_maxExtent!.crossAxisExtent == constraints.crossAxisExtent) { updateChildIfNeeded( constraints.scrollOffset, _maxExtent?.maxExtent, overlapsContent: overlapsContent, ); _layout(); } else { _needsUpdateChild = true; updateChildIfNeeded( constraints.scrollOffset, null, overlapsContent: overlapsContent, ); _rawLayout(); if (constraints.scrollOffset > 0.0) { _needsUpdateChild = true; updateChildIfNeeded( constraints.scrollOffset, _maxExtent?.maxExtent, overlapsContent: overlapsContent, ); _layout(); } } } final childExtent = child!.size.height; final maxExtent = _maxExtent!.maxExtent; final double effectiveRemainingPaintExtent = math.max( 0, constraints.remainingPaintExtent - constraints.overlap, ); final double layoutExtent = clampDouble( maxExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent, ); geometry = SliverGeometry( scrollExtent: maxExtent, paintOrigin: constraints.overlap, paintExtent: math.min(childExtent, effectiveRemainingPaintExtent), layoutExtent: layoutExtent, maxPaintExtent: maxExtent, maxScrollObstructionExtent: minExtent, cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent, hasVisualOverflow: false, ); } @override void paint(PaintingContext context, Offset offset) { if (child != null && geometry!.visible) { context.paintChild(child!, offset); } } @override double childMainAxisPosition(RenderBox child) => 0.0; @override void showOnScreen({ RenderObject? descendant, Rect? rect, Duration duration = Duration.zero, Curve curve = Curves.ease, }) { final Rect? localBounds = descendant != null ? MatrixUtils.transformRect( descendant.getTransformTo(this), rect ?? descendant.paintBounds, ) : rect; final Rect? newRect = _trim(localBounds, top: 0); super.showOnScreen( descendant: this, rect: newRect, duration: duration, curve: curve, ); } } ================================================ FILE: lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart'; import 'package:flutter/widgets.dart'; /// ref [SliverPersistentHeader] abstract class SliverPersistentHeaderDelegate { const SliverPersistentHeaderDelegate(); Widget build( BuildContext context, double shrinkOffset, bool overlapsContent, double? maxExtent, ); double get minExtent; bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate); } class SliverPersistentHeaderElement extends RenderObjectElement { SliverPersistentHeaderElement( SliverPinnedHeader super.widget, ); @override RenderSliverPinnedHeader get renderObject => super.renderObject as RenderSliverPinnedHeader; @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); renderObject.element = this; } @override void unmount() { renderObject.element = null; super.unmount(); } @override void update(SliverPinnedHeader newWidget) { final oldWidget = widget as SliverPinnedHeader; super.update(newWidget); final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate; final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate; if (newDelegate != oldDelegate && (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) { final RenderSliverPinnedHeader renderObject = this.renderObject; _updateChild( newDelegate, renderObject.lastShrinkOffset, renderObject.lastOverlapsContent, renderObject.maxExtent, ); renderObject.triggerRebuild(); } } @override void performRebuild() { super.performRebuild(); renderObject.triggerRebuild(); } Element? child; void _updateChild( SliverPersistentHeaderDelegate delegate, double shrinkOffset, bool overlapsContent, double? maxExtent, ) { final Widget newWidget = delegate.build( this, shrinkOffset, overlapsContent, maxExtent, ); child = updateChild(child, newWidget, null); } void build(double shrinkOffset, bool overlapsContent, double? maxExtent) { owner!.buildScope(this, () { final sliverPersistentHeaderRenderObjectWidget = widget as SliverPinnedHeader; _updateChild( sliverPersistentHeaderRenderObjectWidget.delegate, shrinkOffset, overlapsContent, maxExtent, ); }); } @override void forgetChild(Element child) { assert(child == this.child); this.child = null; super.forgetChild(child); } @override void insertRenderObjectChild(covariant RenderBox child, Object? slot) { assert(renderObject.debugValidateChild(child)); renderObject.child = child; } @override void moveRenderObjectChild( covariant RenderObject child, Object? oldSlot, Object? newSlot, ) { assert(false); } @override void removeRenderObjectChild(covariant RenderObject child, Object? slot) { renderObject.child = null; } @override void visitChildren(ElementVisitor visitor) { if (child != null) { visitor(child!); } } } ================================================ FILE: lib/common/widgets/flutter/chat_list_view.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class ChatListView extends BoxScrollView { ChatListView.separated({ super.key, super.scrollDirection, super.controller, super.primary, super.physics, super.padding, required NullableIndexedWidgetBuilder itemBuilder, @Deprecated( 'Use findItemIndexCallback instead. ' 'findChildIndexCallback returns child indices (which include separators), ' 'while findItemIndexCallback returns item indices (which do not). ' 'If you were multiplying results by 2 to account for separators, ' 'you can remove that workaround when migrating to findItemIndexCallback. ' 'This feature was deprecated after v3.37.0-1.0.pre.', ) ChildIndexGetter? findChildIndexCallback, ChildIndexGetter? findItemIndexCallback, required IndexedWidgetBuilder separatorBuilder, required int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, super.cacheExtent, super.dragStartBehavior, super.keyboardDismissBehavior, super.restorationId, super.clipBehavior, super.hitTestBehavior, }) : assert(itemCount >= 0), assert( findItemIndexCallback == null || findChildIndexCallback == null, 'Cannot provide both findItemIndexCallback and findChildIndexCallback. ' 'Use findItemIndexCallback as findChildIndexCallback is deprecated.', ), childrenDelegate = SliverChildBuilderDelegate( (BuildContext context, int index) { final int itemIndex = index ~/ 2; if (index.isEven) { return itemBuilder(context, itemIndex); } return separatorBuilder(context, itemIndex); }, findChildIndexCallback: findItemIndexCallback != null ? (Key key) { final int? itemIndex = findItemIndexCallback(key); return itemIndex == null ? null : itemIndex * 2; } : findChildIndexCallback, childCount: _computeActualChildCount(itemCount), addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, addSemanticIndexes: addSemanticIndexes, semanticIndexCallback: (Widget widget, int index) { return index.isEven ? index ~/ 2 : null; }, ), super(semanticChildCount: itemCount, reverse: true); final SliverChildDelegate childrenDelegate; @override Widget buildChildLayout(BuildContext context) { return SliverChatList(delegate: childrenDelegate); } static int _computeActualChildCount(int itemCount) { return math.max(0, itemCount * 2 - 1); } } class SliverChatList extends SliverMultiBoxAdaptorWidget { const SliverChatList({super.key, required super.delegate}); @override SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true); @override RenderSliverChatList createRenderObject(BuildContext context) { final element = context as SliverMultiBoxAdaptorElement; return RenderSliverChatList(childManager: element); } } class RenderSliverChatList extends RenderSliverMultiBoxAdaptor with ExtendedRenderObjectMixin { RenderSliverChatList({required super.childManager}); @override void performLayout() { final SliverConstraints constraints = this.constraints; childManager ..didStartLayout() ..setDidUnderflow(false); final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; assert(scrollOffset >= 0.0); final double remainingExtent = constraints.remainingCacheExtent; assert(remainingExtent >= 0.0); final double targetEndScrollOffset = scrollOffset + remainingExtent; final BoxConstraints childConstraints = constraints.asBoxConstraints(); var leadingGarbage = 0; var trailingGarbage = 0; var reachedEnd = false; if (firstChild == null) { if (!addInitialChild()) { geometry = SliverGeometry.zero; childManager.didFinishLayout(); return; } } /// handleCloseToTrailingBegin(); RenderBox? leadingChildWithLayout, trailingChildWithLayout; RenderBox? earliestUsefulChild = firstChild; if (childScrollOffset(firstChild!) == null) { var leadingChildrenWithoutLayoutOffset = 0; while (earliestUsefulChild != null && childScrollOffset(earliestUsefulChild) == null) { earliestUsefulChild = childAfter(earliestUsefulChild); leadingChildrenWithoutLayoutOffset += 1; } collectGarbage(leadingChildrenWithoutLayoutOffset, 0); if (firstChild == null) { if (!addInitialChild()) { geometry = SliverGeometry.zero; childManager.didFinishLayout(); return; } } } earliestUsefulChild = firstChild; for ( double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!; earliestScrollOffset > scrollOffset; earliestScrollOffset = childScrollOffset(earliestUsefulChild)! ) { earliestUsefulChild = insertAndLayoutLeadingChild( childConstraints, parentUsesSize: true, ); if (earliestUsefulChild == null) { final childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = 0.0; if (scrollOffset == 0.0) { firstChild!.layout(childConstraints, parentUsesSize: true); earliestUsefulChild = firstChild; leadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout ??= earliestUsefulChild; break; } else { geometry = SliverGeometry(scrollOffsetCorrection: -scrollOffset); return; } } final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!); if (firstChildScrollOffset < -precisionErrorTolerance) { geometry = SliverGeometry( scrollOffsetCorrection: -firstChildScrollOffset, ); final childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = 0.0; return; } final childParentData = earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = firstChildScrollOffset; assert(earliestUsefulChild == firstChild); leadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout ??= earliestUsefulChild; } assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance); if (scrollOffset < precisionErrorTolerance) { while (indexOf(firstChild!) > 0) { final double earliestScrollOffset = childScrollOffset(firstChild!)!; earliestUsefulChild = insertAndLayoutLeadingChild( childConstraints, parentUsesSize: true, ); assert(earliestUsefulChild != null); final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!); final childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = 0.0; if (firstChildScrollOffset < -precisionErrorTolerance) { geometry = SliverGeometry( scrollOffsetCorrection: -firstChildScrollOffset, ); return; } } } assert(earliestUsefulChild == firstChild); assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset); if (leadingChildWithLayout == null) { earliestUsefulChild!.layout(childConstraints, parentUsesSize: true); leadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout = earliestUsefulChild; } var inLayoutRange = true; var child = earliestUsefulChild; int index = indexOf(child!); double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child); bool advance() { assert(child != null); if (child == trailingChildWithLayout) { inLayoutRange = false; } child = childAfter(child!); if (child == null) { inLayoutRange = false; } index += 1; if (!inLayoutRange) { if (child == null || indexOf(child!) != index) { child = insertAndLayoutChild( childConstraints, after: trailingChildWithLayout, parentUsesSize: true, ); if (child == null) { return false; } } else { child!.layout(childConstraints, parentUsesSize: true); } trailingChildWithLayout = child; } assert(child != null); final childParentData = child!.parentData! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = endScrollOffset; assert(childParentData.index == index); endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!); return true; } while (endScrollOffset < scrollOffset) { leadingGarbage += 1; if (!advance()) { assert(leadingGarbage == childCount); assert(child == null); collectGarbage(leadingGarbage - 1, 0); assert(firstChild == lastChild); final double extent = childScrollOffset(lastChild!)! + paintExtentOf(lastChild!); geometry = SliverGeometry(scrollExtent: extent, maxPaintExtent: extent); return; } } while (endScrollOffset < targetEndScrollOffset) { if (!advance()) { reachedEnd = true; break; } } if (child != null) { child = childAfter(child!); while (child != null) { trailingGarbage += 1; child = childAfter(child!); } } collectGarbage(leadingGarbage, trailingGarbage); assert(debugAssertChildListIsNonEmptyAndContiguous()); final double estimatedMaxScrollOffset; /// endScrollOffset = handleCloseToTrailingEnd(endScrollOffset); if (reachedEnd) { estimatedMaxScrollOffset = endScrollOffset; } else { estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset( constraints, firstIndex: indexOf(firstChild!), lastIndex: indexOf(lastChild!), leadingScrollOffset: childScrollOffset(firstChild!), trailingScrollOffset: endScrollOffset, ); assert( estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild!)!, ); } final double firstChildScrollOffset = childScrollOffset(firstChild!)!; double paintExtent = calculatePaintOffset( constraints, from: firstChildScrollOffset, to: endScrollOffset, ); final double cacheExtent = calculateCacheOffset( constraints, from: firstChildScrollOffset, to: endScrollOffset, ); final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; /// paintExtent += _closeToTrailingDistance; geometry = SliverGeometry( scrollExtent: estimatedMaxScrollOffset, paintExtent: paintExtent, cacheExtent: cacheExtent, maxPaintExtent: estimatedMaxScrollOffset, hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0, ); if (estimatedMaxScrollOffset == endScrollOffset) { childManager.setDidUnderflow(true); } childManager.didFinishLayout(); } } const double kChatListPadding = 14.0; /// from https://github.com/fluttercandies/extended_list mixin ExtendedRenderObjectMixin on RenderSliverMultiBoxAdaptor { void handleCloseToTrailingBegin() { _closeToTrailingDistance = 0.0; } double handleCloseToTrailingEnd(double endScrollOffset) { final extent = constraints.remainingPaintExtent - kChatListPadding; if (endScrollOffset < extent) { _closeToTrailingDistance = extent - endScrollOffset; return extent; } return endScrollOffset; } double _closeToTrailingDistance = 0.0; @override double? childScrollOffset(RenderObject child) { return (super.childScrollOffset(child) ?? 0.0) + _closeToTrailingDistance; } } ================================================ FILE: lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_dyn.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import, depend_on_referenced_packages /// @docImport 'package:flutter/material.dart'; /// @docImport 'package:flutter_test/flutter_test.dart'; /// /// @docImport 'primary_scroll_controller.dart'; /// @docImport 'scroll_configuration.dart'; /// @docImport 'scroll_view.dart'; /// @docImport 'scrollable.dart'; /// @docImport 'single_child_scroll_view.dart'; /// @docImport 'viewport.dart'; library; import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide DraggableScrollableSheet, LayoutBuilder; /// Controls a [DraggableScrollableSheet]. /// /// Draggable scrollable controllers are typically stored as member variables in /// [State] objects and are reused in each [State.build]. Controllers can only /// be used to control one sheet at a time. A controller can be reused with a /// new sheet if the previous sheet has been disposed. /// /// The controller's methods cannot be used until after the controller has been /// passed into a [DraggableScrollableSheet] and the sheet has run initState. /// /// A [DraggableScrollableController] is a [Listenable]. It notifies its /// listeners whenever an attached sheet changes sizes. It does not notify its /// listeners when a sheet is first attached or when an attached sheet's /// parameters change without affecting the sheet's current size. It does not /// fire when [pixels] changes without [size] changing. For example, if the /// constraints provided to an attached sheet change. class DraggableScrollableController extends ChangeNotifier { /// Creates a controller for [DraggableScrollableSheet]. DraggableScrollableController() { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } _DraggableScrollableSheetScrollController? _attachedController; final Set _animationControllers = {}; /// Get the current size (as a fraction of the parent height) of the attached sheet. double get size { _assertAttached(); return _attachedController!.extent.currentSize; } /// Get the current pixel height of the attached sheet. double get pixels { _assertAttached(); return _attachedController!.extent.currentPixels; } /// Convert a sheet's size (fractional value of parent container height) to pixels. double sizeToPixels(double size) { _assertAttached(); return _attachedController!.extent.sizeToPixels(size); } /// Returns Whether any [DraggableScrollableController] objects have attached themselves to the /// [DraggableScrollableSheet]. /// /// If this is false, then members that interact with the [ScrollPosition], /// such as [sizeToPixels], [size], [animateTo], and [jumpTo], must not be /// called. bool get isAttached => _attachedController != null && _attachedController!.hasClients; /// Convert a sheet's pixel height to size (fractional value of parent container height). double pixelsToSize(double pixels) { _assertAttached(); return _attachedController!.extent.pixelsToSize(pixels); } /// Animates the attached sheet from its current size to the given [size], a /// fractional value of the parent container's height. /// /// Any active sheet animation is canceled. If the sheet's internal scrollable /// is currently animating (e.g. responding to a user fling), that animation is /// canceled as well. /// /// An animation will be interrupted whenever the user attempts to scroll /// manually, whenever another activity is started, or when the sheet hits its /// max or min size (e.g. if you animate to 1 but the max size is .8, the /// animation will stop playing when it reaches .8). /// /// The duration must not be zero. To jump to a particular value without an /// animation, use [jumpTo]. /// /// The sheet will not snap after calling [animateTo] even if [DraggableScrollableSheet.snap] /// is true. Snapping only occurs after user drags. /// /// When calling [animateTo] in widget tests, `await`ing the returned /// [Future] may cause the test to hang and timeout. Instead, use /// [WidgetTester.pumpAndSettle]. Future animateTo( double size, { required Duration duration, required Curve curve, }) async { _assertAttached(); assert(size >= 0 && size <= 1); assert(duration != Duration.zero); final animationController = AnimationController.unbounded( vsync: _attachedController!.position.context.vsync, value: _attachedController!.extent.currentSize, ); _animationControllers.add(animationController); _attachedController!.position.goIdle(); // This disables any snapping until the next user interaction with the sheet. _attachedController!.extent.hasDragged = false; _attachedController!.extent.hasChanged = true; _attachedController!.extent.startActivity( onCanceled: () { // Don't stop the controller if it's already finished and may have been disposed. if (animationController.isAnimating) { animationController.stop(); } }, ); animationController.addListener(() { _attachedController!.extent.updateSize( animationController.value, _attachedController!.position.context.notificationContext!, ); }); await animationController.animateTo( clampDouble( size, _attachedController!.extent.minSize, _attachedController!.extent.maxSize, ), duration: duration, curve: curve, ); } /// Jumps the attached sheet from its current size to the given [size], a /// fractional value of the parent container's height. /// /// If [size] is outside of a the attached sheet's min or max child size, /// [jumpTo] will jump the sheet to the nearest valid size instead. /// /// Any active sheet animation is canceled. If the sheet's inner scrollable /// is currently animating (e.g. responding to a user fling), that animation is /// canceled as well. /// /// The sheet will not snap after calling [jumpTo] even if [DraggableScrollableSheet.snap] /// is true. Snapping only occurs after user drags. void jumpTo(double size) { _assertAttached(); assert(size >= 0 && size <= 1); // Call start activity to interrupt any other playing activities. _attachedController!.extent.startActivity(onCanceled: () {}); _attachedController!.position.goIdle(); _attachedController!.extent.hasDragged = false; _attachedController!.extent.hasChanged = true; _attachedController!.extent.updateSize( size, _attachedController!.position.context.notificationContext!, ); } /// Reset the attached sheet to its initial size (see: [DraggableScrollableSheet.initialChildSize]). void reset() { _assertAttached(); _attachedController!.reset(); } void _assertAttached() { assert( isAttached, 'DraggableScrollableController is not attached to a sheet. A DraggableScrollableController ' 'must be used in a DraggableScrollableSheet before any of its methods are called.', ); } void _attach(_DraggableScrollableSheetScrollController scrollController) { assert( _attachedController == null, 'Draggable scrollable controller is already attached to a sheet.', ); _attachedController = scrollController; _attachedController!.extent._currentSize.addListener(notifyListeners); _attachedController!.onPositionDetached = _disposeAnimationControllers; } void _onExtentReplaced(_DraggableSheetExtent previousExtent) { // When the extent has been replaced, the old extent is already disposed and // the controller will point to a new extent. We have to add our listener to // the new extent. _attachedController!.extent._currentSize.addListener(notifyListeners); if (previousExtent.currentSize != _attachedController!.extent.currentSize) { // The listener won't fire for a change in size between two extent // objects so we have to fire it manually here. notifyListeners(); } } void _detach({bool disposeExtent = false}) { if (disposeExtent) { _attachedController?.extent.dispose(); } else { _attachedController?.extent._currentSize.removeListener(notifyListeners); } _disposeAnimationControllers(); _attachedController = null; } void _disposeAnimationControllers() { for (final AnimationController animationController in _animationControllers) { animationController.dispose(); } _animationControllers.clear(); } } /// A container for a [Scrollable] that responds to drag gestures by resizing /// the scrollable until a limit is reached, and then scrolling. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=Hgw819mL_78} /// /// This widget can be dragged along the vertical axis between its /// [minChildSize], which defaults to `0.25` and [maxChildSize], which defaults /// to `1.0`. These sizes are percentages of the height of the parent container. /// /// The widget coordinates resizing and scrolling of the widget returned by /// builder as the user drags along the horizontal axis. /// /// The widget will initially be displayed at its initialChildSize which /// defaults to `0.5`, meaning half the height of its parent. Dragging will work /// between the range of minChildSize and maxChildSize (as percentages of the /// parent container's height) as long as the builder creates a widget which /// uses the provided [ScrollController]. If the widget created by the /// [ScrollableWidgetBuilder] does not use the provided [ScrollController], the /// sheet will remain at the initialChildSize. /// /// By default, the widget will stay at whatever size the user drags it to. To /// make the widget snap to specific sizes whenever they lift their finger /// during a drag, set [snap] to `true`. The sheet will snap between /// [minChildSize] and [maxChildSize]. Use [snapSizes] to add more sizes for /// the sheet to snap between. /// /// The snapping effect is only applied on user drags. Programmatically /// manipulating the sheet size via [DraggableScrollableController.animateTo] or /// [DraggableScrollableController.jumpTo] will ignore [snap] and [snapSizes]. /// /// By default, the widget will expand its non-occupied area to fill available /// space in the parent. If this is not desired, e.g. because the parent wants /// to position sheet based on the space it is taking, the [expand] property /// may be set to false. /// /// {@tool dartpad} /// /// This is a sample widget which shows a [ListView] that has 25 [ListTile]s. /// It starts out as taking up half the body of the [Scaffold], and can be /// dragged up to the full height of the scaffold or down to 25% of the height /// of the scaffold. Upon reaching full height, the list contents will be /// scrolled up or down, until they reach the top of the list again and the user /// drags the sheet back down. /// /// On desktop and web running on desktop platforms, dragging to scroll with a mouse is disabled by default /// to align with the natural behavior found in other desktop applications. /// /// This behavior is dictated by the [ScrollBehavior], and can be changed by adding /// [PointerDeviceKind.mouse] to [ScrollBehavior.dragDevices]. /// For more info on this, please refer to https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag /// /// Alternatively, this example illustrates how to add a drag handle for desktop applications. /// /// ** See code in examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart ** /// {@end-tool} class DraggableScrollableSheet extends StatefulWidget { /// Creates a widget that can be dragged and scrolled in a single gesture. const DraggableScrollableSheet({ super.key, this.initialChildSize = 0.5, this.minChildSize = 0.25, this.maxChildSize = 1.0, this.expand = true, this.snap = false, this.snapSizes, this.snapAnimationDuration, this.controller, this.shouldCloseOnMinExtent = true, required this.builder, }) : assert(minChildSize >= 0.0), assert(maxChildSize <= 1.0), assert(minChildSize <= initialChildSize), assert(initialChildSize <= maxChildSize), assert( snapAnimationDuration == null || snapAnimationDuration > Duration.zero, ); /// The initial fractional value of the parent container's height to use when /// displaying the widget. /// /// Rebuilding the sheet with a new [initialChildSize] will only move /// the sheet to the new value if the sheet has not yet been dragged since it /// was first built or since the last call to [DraggableScrollableActuator.reset]. /// /// The default value is `0.5`. final double initialChildSize; /// The minimum fractional value of the parent container's height to use when /// displaying the widget. /// /// The default value is `0.25`. final double minChildSize; /// The maximum fractional value of the parent container's height to use when /// displaying the widget. /// /// The default value is `1.0`. final double maxChildSize; /// Whether the widget should expand to fill the available space in its parent /// or not. /// /// In most cases, this should be true. However, in the case of a parent /// widget that will position this one based on its desired size (such as a /// [Center]), this should be set to false. /// /// The default value is true. final bool expand; /// Whether the widget should snap between [snapSizes] when the user lifts /// their finger during a drag. /// /// If the user's finger was still moving when they lifted it, the widget will /// snap to the next snap size (see [snapSizes]) in the direction of the drag. /// If their finger was still, the widget will snap to the nearest snap size. /// /// Snapping is not applied when the sheet is programmatically moved by /// calling [DraggableScrollableController.animateTo] or [DraggableScrollableController.jumpTo]. /// /// Rebuilding the sheet with snap newly enabled will immediately trigger a /// snap unless the sheet has not yet been dragged away from /// [initialChildSize] since first being built or since the last call to /// [DraggableScrollableActuator.reset]. final bool snap; /// A list of target sizes that the widget should snap to. /// /// Snap sizes are fractional values of the parent container's height. They /// must be listed in increasing order and be between [minChildSize] and /// [maxChildSize]. /// /// The [minChildSize] and [maxChildSize] are implicitly included in snap /// sizes and do not need to be specified here. For example, `snapSizes = [.5]` /// will result in a sheet that snaps between [minChildSize], `.5`, and /// [maxChildSize]. /// /// Any modifications to the [snapSizes] list will not take effect until the /// `build` function containing this widget is run again. /// /// Rebuilding with a modified or new list will trigger a snap unless the /// sheet has not yet been dragged away from [initialChildSize] since first /// being built or since the last call to [DraggableScrollableActuator.reset]. final List? snapSizes; /// Defines a duration for the snap animations. /// /// If it's not set, then the animation duration is the distance to the snap /// target divided by the velocity of the widget. final Duration? snapAnimationDuration; /// A controller that can be used to programmatically control this sheet. final DraggableScrollableController? controller; /// Whether the sheet, when dragged (or flung) to its minimum size, should /// cause its parent sheet to close. /// /// Set on emitted [DraggableScrollableNotification]s. It is up to parent /// classes to properly read and handle this value. final bool shouldCloseOnMinExtent; /// The builder that creates a child to display in this widget, which will /// use the provided [ScrollController] to enable dragging and scrolling /// of the contents. final ScrollableWidgetBuilder builder; @override State createState() => _DraggableScrollableSheetState(); } /// Manages state between [_DraggableScrollableSheetState], /// [_DraggableScrollableSheetScrollController], and /// [_DraggableScrollableSheetScrollPosition]. /// /// The State knows the pixels available along the axis the widget wants to /// scroll, but expects to get a fraction of those pixels to render the sheet. /// /// The ScrollPosition knows the number of pixels a user wants to move the sheet. /// /// The [currentSize] will never be null. /// The [availablePixels] will never be null, but may be `double.infinity`. class _DraggableSheetExtent { _DraggableSheetExtent({ required this.minSize, required this.maxSize, required this.snap, required this.snapSizes, required this.initialSize, this.snapAnimationDuration, ValueNotifier? currentSize, bool? hasDragged, bool? hasChanged, this.shouldCloseOnMinExtent = true, }) : assert(minSize >= 0), assert(maxSize <= 1), assert(minSize <= initialSize), assert(initialSize <= maxSize), _currentSize = currentSize ?? ValueNotifier(initialSize), availablePixels = double.infinity, hasDragged = hasDragged ?? false, hasChanged = hasChanged ?? false { assert(debugMaybeDispatchCreated('widgets', '_DraggableSheetExtent', this)); } VoidCallback? _cancelActivity; final double minSize; final double maxSize; final bool snap; final List snapSizes; final Duration? snapAnimationDuration; final double initialSize; final bool shouldCloseOnMinExtent; final ValueNotifier _currentSize; double availablePixels; // Used to disable snapping until the user has dragged on the sheet. bool hasDragged; // Used to determine if the sheet should move to a new initial size when it // changes. // We need both `hasChanged` and `hasDragged` to achieve the following // behavior: // 1. The sheet should only snap following user drags (as opposed to // programmatic sheet changes). See docs for `animateTo` and `jumpTo`. // 2. The sheet should move to a new initial child size on rebuild iff the // sheet has not changed, either by drag or programmatic control. See // docs for `initialChildSize`. bool hasChanged; bool get isAtMin => minSize >= _currentSize.value; bool get isAtMax => maxSize <= _currentSize.value; double get currentSize => _currentSize.value; double get currentPixels => sizeToPixels(_currentSize.value); List get pixelSnapSizes => snapSizes.map(sizeToPixels).toList(); /// Start an activity that affects the sheet and register a cancel call back /// that will be called if another activity starts. /// /// The `onCanceled` callback will get called even if the subsequent activity /// started after this one finished, so `onCanceled` must be safe to call at /// any time. void startActivity({required VoidCallback onCanceled}) { _cancelActivity?.call(); _cancelActivity = onCanceled; } /// The scroll position gets inputs in terms of pixels, but the size is /// expected to be expressed as a number between 0..1. /// /// This should only be called to respond to a user drag. To update the /// size in response to a programmatic call, use [updateSize] directly. void addPixelDelta(double delta, BuildContext context) { // Stop any playing sheet animations. _cancelActivity?.call(); _cancelActivity = null; // The user has interacted with the sheet, set `hasDragged` to true so that // we'll snap if applicable. hasDragged = true; hasChanged = true; if (availablePixels == 0) { return; } updateSize(currentSize + pixelsToSize(delta), context); } /// Set the size to the new value. [newSize] should be a number between /// [minSize] and [maxSize]. /// /// This can be triggered by a programmatic (e.g. controller triggered) change /// or a user drag. void updateSize(double newSize, BuildContext context) { final double clampedSize = clampDouble(newSize, minSize, maxSize); if (_currentSize.value == clampedSize) { return; } _currentSize.value = clampedSize; DraggableScrollableNotification( minExtent: minSize, maxExtent: maxSize, extent: currentSize, initialExtent: initialSize, context: context, shouldCloseOnMinExtent: shouldCloseOnMinExtent, ).dispatch(context); } double pixelsToSize(double pixels) { return pixels / availablePixels * maxSize; } double sizeToPixels(double size) { return size / maxSize * availablePixels; } void dispose() { assert(debugMaybeDispatchDisposed(this)); _currentSize.dispose(); } _DraggableSheetExtent copyWith({ required double minSize, required double maxSize, required bool snap, required List snapSizes, required double initialSize, required Duration? snapAnimationDuration, required bool shouldCloseOnMinExtent, }) { return _DraggableSheetExtent( minSize: minSize, maxSize: maxSize, snap: snap, snapSizes: snapSizes, snapAnimationDuration: snapAnimationDuration, initialSize: initialSize, // Set the current size to the possibly updated initial size if the sheet // hasn't changed yet. currentSize: ValueNotifier( hasChanged ? clampDouble(_currentSize.value, minSize, maxSize) : initialSize, ), hasDragged: hasDragged, hasChanged: hasChanged, shouldCloseOnMinExtent: shouldCloseOnMinExtent, ); } } class _DraggableScrollableSheetState extends State { late _DraggableScrollableSheetScrollController _scrollController; late _DraggableSheetExtent _extent; @override void initState() { super.initState(); _extent = _DraggableSheetExtent( minSize: widget.minChildSize, maxSize: widget.maxChildSize, snap: widget.snap, snapSizes: _impliedSnapSizes(), snapAnimationDuration: widget.snapAnimationDuration, initialSize: widget.initialChildSize, shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, ); _scrollController = _DraggableScrollableSheetScrollController( extent: _extent, ); widget.controller?._attach(_scrollController); } List _impliedSnapSizes() { for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { final double snapSize = widget.snapSizes![index]; assert( snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize, '${_snapSizeErrorMessage(index)}\nSnap sizes must be between `minChildSize` and `maxChildSize`. ', ); assert( index == 0 || snapSize > widget.snapSizes![index - 1], '${_snapSizeErrorMessage(index)}\nSnap sizes must be in ascending order. ', ); } // Ensure the snap sizes start and end with the min and max child sizes. if (widget.snapSizes == null || widget.snapSizes!.isEmpty) { return [widget.minChildSize, widget.maxChildSize]; } return [ if (widget.snapSizes!.first != widget.minChildSize) widget.minChildSize, ...widget.snapSizes!, if (widget.snapSizes!.last != widget.maxChildSize) widget.maxChildSize, ]; } @override void didUpdateWidget(covariant DraggableScrollableSheet oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { oldWidget.controller?._detach(); widget.controller?._attach(_scrollController); } _replaceExtent(oldWidget); } @override void didChangeDependencies() { super.didChangeDependencies(); if (_InheritedResetNotifier.shouldReset(context)) { _scrollController.reset(); } } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: _extent._currentSize, builder: (BuildContext context, double currentSize, Widget? child) => LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { _extent.availablePixels = widget.maxChildSize * constraints.biggest.height; final Widget sheet = FractionallySizedBox( heightFactor: currentSize, alignment: Alignment.bottomCenter, child: child, ); return widget.expand ? SizedBox.expand(child: sheet) : sheet; }, ), child: widget.builder(context, _scrollController), ); } @override void dispose() { if (widget.controller == null) { _extent.dispose(); } else { widget.controller!._detach(disposeExtent: true); } _scrollController.dispose(); super.dispose(); } void _replaceExtent(covariant DraggableScrollableSheet oldWidget) { final _DraggableSheetExtent previousExtent = _extent; _extent = previousExtent.copyWith( minSize: widget.minChildSize, maxSize: widget.maxChildSize, snap: widget.snap, snapSizes: _impliedSnapSizes(), snapAnimationDuration: widget.snapAnimationDuration, initialSize: widget.initialChildSize, shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, ); // Modify the existing scroll controller instead of replacing it so that // developers listening to the controller do not have to rebuild their listeners. _scrollController.extent = _extent; // If an external facing controller was provided, let it know that the // extent has been replaced. widget.controller?._onExtentReplaced(previousExtent); previousExtent.dispose(); if (widget.snap && (widget.snap != oldWidget.snap || widget.snapSizes != oldWidget.snapSizes) && _scrollController.hasClients) { // Trigger a snap in case snap or snapSizes has changed and there is a // scroll position currently attached. We put this in a post frame // callback so that `build` can update `_extent.availablePixels` before // this runs-we can't use the previous extent's available pixels as it may // have changed when the widget was updated. WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { for ( var index = 0; index < _scrollController.positions.length; index++ ) { final position = _scrollController.positions.elementAt(index) as _DraggableScrollableSheetScrollPosition; position.goBallistic(0); } }, debugLabel: 'DraggableScrollableSheet.snap'); } } String _snapSizeErrorMessage(int invalidIndex) { final List snapSizesWithIndicator = widget.snapSizes! .asMap() .keys .map((int index) { final snapSizeString = widget.snapSizes![index].toString(); if (index == invalidIndex) { return '>>> $snapSizeString <<<'; } return snapSizeString; }) .toList(); return "Invalid snapSize '${widget.snapSizes![invalidIndex]}' at index $invalidIndex of:\n" ' $snapSizesWithIndicator'; } } /// A [ScrollController] suitable for use in a [ScrollableWidgetBuilder] created /// by a [DraggableScrollableSheet]. /// /// If a [DraggableScrollableSheet] contains content that is exceeds the height /// of its container, this controller will allow the sheet to both be dragged to /// fill the container and then scroll the child content. /// /// See also: /// /// * [_DraggableScrollableSheetScrollPosition], which manages the positioning logic for /// this controller. /// * [PrimaryScrollController], which can be used to establish a /// [_DraggableScrollableSheetScrollController] as the primary controller for /// descendants. class _DraggableScrollableSheetScrollController extends ScrollController { _DraggableScrollableSheetScrollController({ required this.extent, }); _DraggableSheetExtent extent; VoidCallback? onPositionDetached; @override _DraggableScrollableSheetScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition, ) { return _DraggableScrollableSheetScrollPosition( physics: physics.applyTo(const AlwaysScrollableScrollPhysics()), context: context, oldPosition: oldPosition, getExtent: () => extent, ); } @override void debugFillDescription(List description) { super.debugFillDescription(description); description.add('extent: $extent'); } @override _DraggableScrollableSheetScrollPosition get position => super.position as _DraggableScrollableSheetScrollPosition; void reset() { extent._cancelActivity?.call(); extent.hasDragged = false; extent.hasChanged = false; // jumpTo can result in trying to replace semantics during build. // Just animate really fast. // Avoid doing it at all if the offset is already 0.0. if (offset != 0.0) { animateTo( 0.0, duration: const Duration(milliseconds: 1), curve: Curves.linear, ); } extent.updateSize( extent.initialSize, position.context.notificationContext!, ); } @override void detach(ScrollPosition position) { onPositionDetached?.call(); super.detach(position); } } /// A scroll position that manages scroll activities for /// [_DraggableScrollableSheetScrollController]. /// /// This class is a concrete subclass of [ScrollPosition] logic that handles a /// single [ScrollContext], such as a [Scrollable]. An instance of this class /// manages [ScrollActivity] instances, which changes the /// [_DraggableSheetExtent.currentSize] or visible content offset in the /// [Scrollable]'s [Viewport] /// /// See also: /// /// * [_DraggableScrollableSheetScrollController], which uses this as its [ScrollPosition]. class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleContext { _DraggableScrollableSheetScrollPosition({ required super.physics, required super.context, super.oldPosition, required this.getExtent, }); VoidCallback? _dragCancelCallback; final _DraggableSheetExtent Function() getExtent; final Set _ballisticControllers = {}; bool get listShouldScroll => pixels > 0.0; _DraggableSheetExtent get extent => getExtent(); bool _isAtTop = true; @override void absorb(ScrollPosition other) { super.absorb(other); assert(_dragCancelCallback == null); if (other is! _DraggableScrollableSheetScrollPosition) { return; } if (other._dragCancelCallback != null) { _dragCancelCallback = other._dragCancelCallback; other._dragCancelCallback = null; } } @override void beginActivity(ScrollActivity? newActivity) { // Cancel the running ballistic simulations for (final AnimationController ballisticController in _ballisticControllers) { ballisticController.stop(); } super.beginActivity(newActivity); } @override void applyUserOffset(double delta) { if (!_isAtTop) { super.applyUserOffset(delta); } else if (!listShouldScroll && (!(extent.isAtMin || extent.isAtMax) || (extent.isAtMin && delta < 0) || (extent.isAtMax && delta > 0))) { extent.addPixelDelta(-delta, context.notificationContext!); } else { super.applyUserOffset(delta); } } // Checks if the sheet's current size is close to a snap size, returning the // snap size if so; returns null otherwise. double? _getCurrentSnapSize() { return extent.snapSizes.firstWhereOrNull((double snapSize) { return (extent.currentSize - snapSize).abs() <= extent.pixelsToSize(physics.toleranceFor(this).distance); }); } bool _isAtSnapSize() => _getCurrentSnapSize() != null; bool _shouldSnap() => extent.snap && extent.hasDragged && !_isAtSnapSize(); @override void dispose() { for (final AnimationController ballisticController in _ballisticControllers) { ballisticController.dispose(); } _ballisticControllers.clear(); super.dispose(); } @override void goBallistic(double velocity) { if (!_isAtTop) { super.goBallistic(velocity); return; } if ((velocity == 0.0 && !_shouldSnap()) || (velocity < 0.0 && listShouldScroll) || (velocity > 0.0 && extent.isAtMax)) { super.goBallistic(velocity); return; } // Scrollable expects that we will dispose of its current _dragCancelCallback _dragCancelCallback?.call(); _dragCancelCallback = null; late final Simulation simulation; if (extent.snap) { // Snap is enabled, simulate snapping instead of clamping scroll. simulation = _SnappingSimulation( position: extent.currentPixels, initialVelocity: velocity, pixelSnapSize: extent.pixelSnapSizes, snapAnimationDuration: extent.snapAnimationDuration, tolerance: physics.toleranceFor(this), ); } else { // The iOS bouncing simulation just isn't right here - once we delegate // the ballistic back to the ScrollView, it will use the right simulation. simulation = ClampingScrollSimulation( // Run the simulation in terms of pixels, not extent. position: extent.currentPixels, velocity: velocity, tolerance: physics.toleranceFor(this), ); } final ballisticController = AnimationController.unbounded( debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'), vsync: context.vsync, ); _ballisticControllers.add(ballisticController); double lastPosition = extent.currentPixels; void tick() { final double delta = ballisticController.value - lastPosition; lastPosition = ballisticController.value; extent.addPixelDelta(delta, context.notificationContext!); if ((velocity > 0 && extent.isAtMax) || (velocity < 0 && extent.isAtMin)) { // Make sure we pass along enough velocity to keep scrolling - otherwise // we just "bounce" off the top making it look like the list doesn't // have more to scroll. velocity = ballisticController.velocity + (physics.toleranceFor(this).velocity * ballisticController.velocity.sign); super.goBallistic(velocity); ballisticController.stop(); } else if (ballisticController.isCompleted) { // Update the extent value after the snap animation completes to // avoid rounding errors that could prevent the sheet from closing when // it reaches minSize. final double? snapSize = _getCurrentSnapSize(); if (snapSize != null) { extent.updateSize(snapSize, context.notificationContext!); } super.goBallistic(0); } } ballisticController ..addListener(tick) ..animateWith(simulation).whenCompleteOrCancel(() { if (_ballisticControllers.contains(ballisticController)) { _ballisticControllers.remove(ballisticController); ballisticController.dispose(); } }); } @override Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { _isAtTop = pixels == 0; // Save this so we can call it later if we have to [goBallistic] on our own. _dragCancelCallback = dragCancelCallback; return super.drag(details, dragCancelCallback); } } /// A widget that can notify a descendent [DraggableScrollableSheet] that it /// should reset its position to the initial state. /// /// The [Scaffold] uses this widget to notify a persistent bottom sheet that /// the user has tapped back if the sheet has started to cover more of the body /// than when at its initial position. This is important for users of assistive /// technology, where dragging may be difficult to communicate. /// /// This is just a wrapper on top of [DraggableScrollableController]. It is /// primarily useful for controlling a sheet in a part of the widget tree that /// the current code does not control (e.g. library code trying to affect a sheet /// in library users' code). Generally, it's easier to control the sheet /// directly by creating a controller and passing the controller to the sheet in /// its constructor (see [DraggableScrollableSheet.controller]). class DraggableScrollableActuator extends StatefulWidget { /// Creates a widget that can notify descendent [DraggableScrollableSheet]s /// to reset to their initial position. /// /// The [child] parameter is required. const DraggableScrollableActuator({super.key, required this.child}); /// This child's [DraggableScrollableSheet] descendant will be reset when the /// [reset] method is applied to a context that includes it. final Widget child; /// Notifies any descendant [DraggableScrollableSheet] that it should reset /// to its initial position. /// /// Returns `true` if a [DraggableScrollableActuator] is available and /// some [DraggableScrollableSheet] is listening for updates, `false` /// otherwise. static bool reset(BuildContext context) { final _InheritedResetNotifier? notifier = context .dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>(); return notifier?._sendReset() ?? false; } @override State createState() => _DraggableScrollableActuatorState(); } class _DraggableScrollableActuatorState extends State { final _ResetNotifier _notifier = _ResetNotifier(); @override Widget build(BuildContext context) { return _InheritedResetNotifier(notifier: _notifier, child: widget.child); } @override void dispose() { _notifier.dispose(); super.dispose(); } } /// A [ChangeNotifier] to use with [_InheritedResetNotifier] to notify /// descendants that they should reset to initial state. class _ResetNotifier extends ChangeNotifier { _ResetNotifier() { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } /// Whether someone called [sendReset] or not. /// /// This flag should be reset after checking it. bool _wasCalled = false; /// Fires a reset notification to descendants. /// /// Returns false if there are no listeners. bool sendReset() { if (!hasListeners) { return false; } _wasCalled = true; notifyListeners(); return true; } } class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> { /// Creates an [InheritedNotifier] that the [DraggableScrollableSheet] will /// listen to for an indication that it should reset itself back to [DraggableScrollableSheet.initialChildSize]. const _InheritedResetNotifier({ required super.child, required _ResetNotifier super.notifier, }); bool _sendReset() => notifier!.sendReset(); /// Specifies whether the [DraggableScrollableSheet] should reset to its /// initial position. /// /// Returns true if the notifier requested a reset, false otherwise. static bool shouldReset(BuildContext context) { final InheritedWidget? widget = context .dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>(); if (widget == null) { return false; } assert(widget is _InheritedResetNotifier); final inheritedNotifier = widget as _InheritedResetNotifier; final bool wasCalled = inheritedNotifier.notifier!._wasCalled; inheritedNotifier.notifier!._wasCalled = false; return wasCalled; } } class _SnappingSimulation extends Simulation { _SnappingSimulation({ required this.position, required double initialVelocity, required List pixelSnapSize, Duration? snapAnimationDuration, super.tolerance, }) { _pixelSnapSize = _getSnapSize(initialVelocity, pixelSnapSize); if (snapAnimationDuration != null && snapAnimationDuration.inMilliseconds > 0) { velocity = (_pixelSnapSize - position) * 1000 / snapAnimationDuration.inMilliseconds; } // Check the direction of the target instead of the sign of the velocity because // we may snap in the opposite direction of velocity if velocity is very low. else if (_pixelSnapSize < position) { velocity = math.min(-minimumSpeed, initialVelocity); } else { velocity = math.max(minimumSpeed, initialVelocity); } } final double position; late final double velocity; // A minimum speed to snap at. Used to ensure that the snapping animation // does not play too slowly. static const double minimumSpeed = 1600.0; late final double _pixelSnapSize; @override double dx(double time) { if (isDone(time)) { return 0; } return velocity; } @override bool isDone(double time) { return x(time) == _pixelSnapSize; } @override double x(double time) { final double newPosition = position + velocity * time; if ((velocity >= 0 && newPosition > _pixelSnapSize) || (velocity < 0 && newPosition < _pixelSnapSize)) { // We're passed the snap size, return it instead. return _pixelSnapSize; } return newPosition; } // Find the two closest snap sizes to the position. If the velocity is // non-zero, select the size in the velocity's direction. Otherwise, // the nearest snap size. double _getSnapSize(double initialVelocity, List pixelSnapSizes) { final int indexOfNextSize = pixelSnapSizes.indexWhere( (double size) => size >= position, ); if (indexOfNextSize == 0) { return pixelSnapSizes.first; } final double nextSize = pixelSnapSizes[indexOfNextSize]; // If already snapped - keep this as target size if (nextSize == position) { return nextSize; } final double previousSize = pixelSnapSizes[indexOfNextSize - 1]; if (initialVelocity.abs() <= tolerance.velocity) { // If velocity is zero, snap to the nearest snap size with the minimum velocity. if (position - previousSize < nextSize - position) { return previousSize; } else { return nextSize; } } // Snap forward or backward depending on current velocity. if (initialVelocity < 0.0) { return pixelSnapSizes[indexOfNextSize - 1]; } return pixelSnapSizes[indexOfNextSize]; } } ================================================ FILE: lib/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import, depend_on_referenced_packages /// @docImport 'package:flutter/material.dart'; /// @docImport 'package:flutter_test/flutter_test.dart'; /// /// @docImport 'primary_scroll_controller.dart'; /// @docImport 'scroll_configuration.dart'; /// @docImport 'scroll_view.dart'; /// @docImport 'scrollable.dart'; /// @docImport 'single_child_scroll_view.dart'; /// @docImport 'viewport.dart'; library; import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide DraggableScrollableSheet, LayoutBuilder; /// Controls a [DraggableScrollableSheet]. /// /// Draggable scrollable controllers are typically stored as member variables in /// [State] objects and are reused in each [State.build]. Controllers can only /// be used to control one sheet at a time. A controller can be reused with a /// new sheet if the previous sheet has been disposed. /// /// The controller's methods cannot be used until after the controller has been /// passed into a [DraggableScrollableSheet] and the sheet has run initState. /// /// A [DraggableScrollableController] is a [Listenable]. It notifies its /// listeners whenever an attached sheet changes sizes. It does not notify its /// listeners when a sheet is first attached or when an attached sheet's /// parameters change without affecting the sheet's current size. It does not /// fire when [pixels] changes without [size] changing. For example, if the /// constraints provided to an attached sheet change. class DraggableScrollableController extends ChangeNotifier { /// Creates a controller for [DraggableScrollableSheet]. DraggableScrollableController() { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } _DraggableScrollableSheetScrollController? _attachedController; final Set _animationControllers = {}; /// Get the current size (as a fraction of the parent height) of the attached sheet. double get size { _assertAttached(); return _attachedController!.extent.currentSize; } /// Get the current pixel height of the attached sheet. double get pixels { _assertAttached(); return _attachedController!.extent.currentPixels; } /// Convert a sheet's size (fractional value of parent container height) to pixels. double sizeToPixels(double size) { _assertAttached(); return _attachedController!.extent.sizeToPixels(size); } /// Returns Whether any [DraggableScrollableController] objects have attached themselves to the /// [DraggableScrollableSheet]. /// /// If this is false, then members that interact with the [ScrollPosition], /// such as [sizeToPixels], [size], [animateTo], and [jumpTo], must not be /// called. bool get isAttached => _attachedController != null && _attachedController!.hasClients; /// Convert a sheet's pixel height to size (fractional value of parent container height). double pixelsToSize(double pixels) { _assertAttached(); return _attachedController!.extent.pixelsToSize(pixels); } /// Animates the attached sheet from its current size to the given [size], a /// fractional value of the parent container's height. /// /// Any active sheet animation is canceled. If the sheet's internal scrollable /// is currently animating (e.g. responding to a user fling), that animation is /// canceled as well. /// /// An animation will be interrupted whenever the user attempts to scroll /// manually, whenever another activity is started, or when the sheet hits its /// max or min size (e.g. if you animate to 1 but the max size is .8, the /// animation will stop playing when it reaches .8). /// /// The duration must not be zero. To jump to a particular value without an /// animation, use [jumpTo]. /// /// The sheet will not snap after calling [animateTo] even if [DraggableScrollableSheet.snap] /// is true. Snapping only occurs after user drags. /// /// When calling [animateTo] in widget tests, `await`ing the returned /// [Future] may cause the test to hang and timeout. Instead, use /// [WidgetTester.pumpAndSettle]. Future animateTo( double size, { required Duration duration, required Curve curve, }) async { _assertAttached(); assert(size >= 0 && size <= 1); assert(duration != Duration.zero); final animationController = AnimationController.unbounded( vsync: _attachedController!.position.context.vsync, value: _attachedController!.extent.currentSize, ); _animationControllers.add(animationController); _attachedController!.position.goIdle(); // This disables any snapping until the next user interaction with the sheet. _attachedController!.extent.hasDragged = false; _attachedController!.extent.hasChanged = true; _attachedController!.extent.startActivity( onCanceled: () { // Don't stop the controller if it's already finished and may have been disposed. if (animationController.isAnimating) { animationController.stop(); } }, ); animationController.addListener(() { _attachedController!.extent.updateSize( animationController.value, _attachedController!.position.context.notificationContext!, ); }); await animationController.animateTo( clampDouble( size, _attachedController!.extent.minSize, _attachedController!.extent.maxSize, ), duration: duration, curve: curve, ); } /// Jumps the attached sheet from its current size to the given [size], a /// fractional value of the parent container's height. /// /// If [size] is outside of a the attached sheet's min or max child size, /// [jumpTo] will jump the sheet to the nearest valid size instead. /// /// Any active sheet animation is canceled. If the sheet's inner scrollable /// is currently animating (e.g. responding to a user fling), that animation is /// canceled as well. /// /// The sheet will not snap after calling [jumpTo] even if [DraggableScrollableSheet.snap] /// is true. Snapping only occurs after user drags. void jumpTo(double size) { _assertAttached(); assert(size >= 0 && size <= 1); // Call start activity to interrupt any other playing activities. _attachedController!.extent.startActivity(onCanceled: () {}); _attachedController!.position.goIdle(); _attachedController!.extent.hasDragged = false; _attachedController!.extent.hasChanged = true; _attachedController!.extent.updateSize( size, _attachedController!.position.context.notificationContext!, ); } /// Reset the attached sheet to its initial size (see: [DraggableScrollableSheet.initialChildSize]). void reset() { _assertAttached(); _attachedController!.reset(); } void _assertAttached() { assert( isAttached, 'DraggableScrollableController is not attached to a sheet. A DraggableScrollableController ' 'must be used in a DraggableScrollableSheet before any of its methods are called.', ); } void _attach(_DraggableScrollableSheetScrollController scrollController) { assert( _attachedController == null, 'Draggable scrollable controller is already attached to a sheet.', ); _attachedController = scrollController; _attachedController!.extent._currentSize.addListener(notifyListeners); _attachedController!.onPositionDetached = _disposeAnimationControllers; } void _onExtentReplaced(_DraggableSheetExtent previousExtent) { // When the extent has been replaced, the old extent is already disposed and // the controller will point to a new extent. We have to add our listener to // the new extent. _attachedController!.extent._currentSize.addListener(notifyListeners); if (previousExtent.currentSize != _attachedController!.extent.currentSize) { // The listener won't fire for a change in size between two extent // objects so we have to fire it manually here. notifyListeners(); } } void _detach({bool disposeExtent = false}) { if (disposeExtent) { _attachedController?.extent.dispose(); } else { _attachedController?.extent._currentSize.removeListener(notifyListeners); } _disposeAnimationControllers(); _attachedController = null; } void _disposeAnimationControllers() { for (final AnimationController animationController in _animationControllers) { animationController.dispose(); } _animationControllers.clear(); } } /// A container for a [Scrollable] that responds to drag gestures by resizing /// the scrollable until a limit is reached, and then scrolling. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=Hgw819mL_78} /// /// This widget can be dragged along the vertical axis between its /// [minChildSize], which defaults to `0.25` and [maxChildSize], which defaults /// to `1.0`. These sizes are percentages of the height of the parent container. /// /// The widget coordinates resizing and scrolling of the widget returned by /// builder as the user drags along the horizontal axis. /// /// The widget will initially be displayed at its initialChildSize which /// defaults to `0.5`, meaning half the height of its parent. Dragging will work /// between the range of minChildSize and maxChildSize (as percentages of the /// parent container's height) as long as the builder creates a widget which /// uses the provided [ScrollController]. If the widget created by the /// [ScrollableWidgetBuilder] does not use the provided [ScrollController], the /// sheet will remain at the initialChildSize. /// /// By default, the widget will stay at whatever size the user drags it to. To /// make the widget snap to specific sizes whenever they lift their finger /// during a drag, set [snap] to `true`. The sheet will snap between /// [minChildSize] and [maxChildSize]. Use [snapSizes] to add more sizes for /// the sheet to snap between. /// /// The snapping effect is only applied on user drags. Programmatically /// manipulating the sheet size via [DraggableScrollableController.animateTo] or /// [DraggableScrollableController.jumpTo] will ignore [snap] and [snapSizes]. /// /// By default, the widget will expand its non-occupied area to fill available /// space in the parent. If this is not desired, e.g. because the parent wants /// to position sheet based on the space it is taking, the [expand] property /// may be set to false. /// /// {@tool dartpad} /// /// This is a sample widget which shows a [ListView] that has 25 [ListTile]s. /// It starts out as taking up half the body of the [Scaffold], and can be /// dragged up to the full height of the scaffold or down to 25% of the height /// of the scaffold. Upon reaching full height, the list contents will be /// scrolled up or down, until they reach the top of the list again and the user /// drags the sheet back down. /// /// On desktop and web running on desktop platforms, dragging to scroll with a mouse is disabled by default /// to align with the natural behavior found in other desktop applications. /// /// This behavior is dictated by the [ScrollBehavior], and can be changed by adding /// [PointerDeviceKind.mouse] to [ScrollBehavior.dragDevices]. /// For more info on this, please refer to https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag /// /// Alternatively, this example illustrates how to add a drag handle for desktop applications. /// /// ** See code in examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart ** /// {@end-tool} class DraggableScrollableSheet extends StatefulWidget { /// Creates a widget that can be dragged and scrolled in a single gesture. const DraggableScrollableSheet({ super.key, this.initialChildSize = 0.5, this.minChildSize = 0.25, this.maxChildSize = 1.0, this.expand = true, this.snap = false, this.snapSizes, this.snapAnimationDuration, this.controller, this.shouldCloseOnMinExtent = true, this.initialScrollOffset = 0, required this.builder, }) : assert(minChildSize >= 0.0), assert(maxChildSize <= 1.0), assert(minChildSize <= initialChildSize), assert(initialChildSize <= maxChildSize), assert( snapAnimationDuration == null || snapAnimationDuration > Duration.zero, ); final double initialScrollOffset; /// The initial fractional value of the parent container's height to use when /// displaying the widget. /// /// Rebuilding the sheet with a new [initialChildSize] will only move /// the sheet to the new value if the sheet has not yet been dragged since it /// was first built or since the last call to [DraggableScrollableActuator.reset]. /// /// The default value is `0.5`. final double initialChildSize; /// The minimum fractional value of the parent container's height to use when /// displaying the widget. /// /// The default value is `0.25`. final double minChildSize; /// The maximum fractional value of the parent container's height to use when /// displaying the widget. /// /// The default value is `1.0`. final double maxChildSize; /// Whether the widget should expand to fill the available space in its parent /// or not. /// /// In most cases, this should be true. However, in the case of a parent /// widget that will position this one based on its desired size (such as a /// [Center]), this should be set to false. /// /// The default value is true. final bool expand; /// Whether the widget should snap between [snapSizes] when the user lifts /// their finger during a drag. /// /// If the user's finger was still moving when they lifted it, the widget will /// snap to the next snap size (see [snapSizes]) in the direction of the drag. /// If their finger was still, the widget will snap to the nearest snap size. /// /// Snapping is not applied when the sheet is programmatically moved by /// calling [DraggableScrollableController.animateTo] or [DraggableScrollableController.jumpTo]. /// /// Rebuilding the sheet with snap newly enabled will immediately trigger a /// snap unless the sheet has not yet been dragged away from /// [initialChildSize] since first being built or since the last call to /// [DraggableScrollableActuator.reset]. final bool snap; /// A list of target sizes that the widget should snap to. /// /// Snap sizes are fractional values of the parent container's height. They /// must be listed in increasing order and be between [minChildSize] and /// [maxChildSize]. /// /// The [minChildSize] and [maxChildSize] are implicitly included in snap /// sizes and do not need to be specified here. For example, `snapSizes = [.5]` /// will result in a sheet that snaps between [minChildSize], `.5`, and /// [maxChildSize]. /// /// Any modifications to the [snapSizes] list will not take effect until the /// `build` function containing this widget is run again. /// /// Rebuilding with a modified or new list will trigger a snap unless the /// sheet has not yet been dragged away from [initialChildSize] since first /// being built or since the last call to [DraggableScrollableActuator.reset]. final List? snapSizes; /// Defines a duration for the snap animations. /// /// If it's not set, then the animation duration is the distance to the snap /// target divided by the velocity of the widget. final Duration? snapAnimationDuration; /// A controller that can be used to programmatically control this sheet. final DraggableScrollableController? controller; /// Whether the sheet, when dragged (or flung) to its minimum size, should /// cause its parent sheet to close. /// /// Set on emitted [DraggableScrollableNotification]s. It is up to parent /// classes to properly read and handle this value. final bool shouldCloseOnMinExtent; /// The builder that creates a child to display in this widget, which will /// use the provided [ScrollController] to enable dragging and scrolling /// of the contents. final ScrollableWidgetBuilder builder; @override State createState() => _DraggableScrollableSheetState(); } /// Manages state between [_DraggableScrollableSheetState], /// [_DraggableScrollableSheetScrollController], and /// [_DraggableScrollableSheetScrollPosition]. /// /// The State knows the pixels available along the axis the widget wants to /// scroll, but expects to get a fraction of those pixels to render the sheet. /// /// The ScrollPosition knows the number of pixels a user wants to move the sheet. /// /// The [currentSize] will never be null. /// The [availablePixels] will never be null, but may be `double.infinity`. class _DraggableSheetExtent { _DraggableSheetExtent({ required this.minSize, required this.maxSize, required this.snap, required this.snapSizes, required this.initialSize, this.snapAnimationDuration, ValueNotifier? currentSize, bool? hasDragged, bool? hasChanged, this.shouldCloseOnMinExtent = true, }) : assert(minSize >= 0), assert(maxSize <= 1), assert(minSize <= initialSize), assert(initialSize <= maxSize), _currentSize = currentSize ?? ValueNotifier(initialSize), availablePixels = double.infinity, hasDragged = hasDragged ?? false, hasChanged = hasChanged ?? false { assert(debugMaybeDispatchCreated('widgets', '_DraggableSheetExtent', this)); } VoidCallback? _cancelActivity; final double minSize; final double maxSize; final bool snap; final List snapSizes; final Duration? snapAnimationDuration; final double initialSize; final bool shouldCloseOnMinExtent; final ValueNotifier _currentSize; double availablePixels; // Used to disable snapping until the user has dragged on the sheet. bool hasDragged; // Used to determine if the sheet should move to a new initial size when it // changes. // We need both `hasChanged` and `hasDragged` to achieve the following // behavior: // 1. The sheet should only snap following user drags (as opposed to // programmatic sheet changes). See docs for `animateTo` and `jumpTo`. // 2. The sheet should move to a new initial child size on rebuild iff the // sheet has not changed, either by drag or programmatic control. See // docs for `initialChildSize`. bool hasChanged; bool get isAtMin => minSize >= _currentSize.value; bool get isAtMax => maxSize <= _currentSize.value; double get currentSize => _currentSize.value; double get currentPixels => sizeToPixels(_currentSize.value); List get pixelSnapSizes => snapSizes.map(sizeToPixels).toList(); /// Start an activity that affects the sheet and register a cancel call back /// that will be called if another activity starts. /// /// The `onCanceled` callback will get called even if the subsequent activity /// started after this one finished, so `onCanceled` must be safe to call at /// any time. void startActivity({required VoidCallback onCanceled}) { _cancelActivity?.call(); _cancelActivity = onCanceled; } /// The scroll position gets inputs in terms of pixels, but the size is /// expected to be expressed as a number between 0..1. /// /// This should only be called to respond to a user drag. To update the /// size in response to a programmatic call, use [updateSize] directly. void addPixelDelta(double delta, BuildContext context) { // Stop any playing sheet animations. _cancelActivity?.call(); _cancelActivity = null; // The user has interacted with the sheet, set `hasDragged` to true so that // we'll snap if applicable. hasDragged = true; hasChanged = true; if (availablePixels == 0) { return; } updateSize(currentSize + pixelsToSize(delta), context); } /// Set the size to the new value. [newSize] should be a number between /// [minSize] and [maxSize]. /// /// This can be triggered by a programmatic (e.g. controller triggered) change /// or a user drag. void updateSize(double newSize, BuildContext context) { final double clampedSize = clampDouble(newSize, minSize, maxSize); if (_currentSize.value == clampedSize) { return; } _currentSize.value = clampedSize; DraggableScrollableNotification( minExtent: minSize, maxExtent: maxSize, extent: currentSize, initialExtent: initialSize, context: context, shouldCloseOnMinExtent: shouldCloseOnMinExtent, ).dispatch(context); } double pixelsToSize(double pixels) { return pixels / availablePixels * maxSize; } double sizeToPixels(double size) { return size / maxSize * availablePixels; } void dispose() { assert(debugMaybeDispatchDisposed(this)); _currentSize.dispose(); } _DraggableSheetExtent copyWith({ required double minSize, required double maxSize, required bool snap, required List snapSizes, required double initialSize, required Duration? snapAnimationDuration, required bool shouldCloseOnMinExtent, }) { return _DraggableSheetExtent( minSize: minSize, maxSize: maxSize, snap: snap, snapSizes: snapSizes, snapAnimationDuration: snapAnimationDuration, initialSize: initialSize, // Set the current size to the possibly updated initial size if the sheet // hasn't changed yet. currentSize: ValueNotifier( hasChanged ? clampDouble(_currentSize.value, minSize, maxSize) : initialSize, ), hasDragged: hasDragged, hasChanged: hasChanged, shouldCloseOnMinExtent: shouldCloseOnMinExtent, ); } } class _DraggableScrollableSheetState extends State { late _DraggableScrollableSheetScrollController _scrollController; late _DraggableSheetExtent _extent; @override void initState() { super.initState(); _extent = _DraggableSheetExtent( minSize: widget.minChildSize, maxSize: widget.maxChildSize, snap: widget.snap, snapSizes: _impliedSnapSizes(), snapAnimationDuration: widget.snapAnimationDuration, initialSize: widget.initialChildSize, shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, ); _scrollController = _DraggableScrollableSheetScrollController( extent: _extent, initialScrollOffset: widget.initialScrollOffset, ); widget.controller?._attach(_scrollController); } List _impliedSnapSizes() { for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { final double snapSize = widget.snapSizes![index]; assert( snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize, '${_snapSizeErrorMessage(index)}\nSnap sizes must be between `minChildSize` and `maxChildSize`. ', ); assert( index == 0 || snapSize > widget.snapSizes![index - 1], '${_snapSizeErrorMessage(index)}\nSnap sizes must be in ascending order. ', ); } // Ensure the snap sizes start and end with the min and max child sizes. if (widget.snapSizes == null || widget.snapSizes!.isEmpty) { return [widget.minChildSize, widget.maxChildSize]; } return [ if (widget.snapSizes!.first != widget.minChildSize) widget.minChildSize, ...widget.snapSizes!, if (widget.snapSizes!.last != widget.maxChildSize) widget.maxChildSize, ]; } @override void didUpdateWidget(covariant DraggableScrollableSheet oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { oldWidget.controller?._detach(); widget.controller?._attach(_scrollController); } _replaceExtent(oldWidget); } @override void didChangeDependencies() { super.didChangeDependencies(); if (_InheritedResetNotifier.shouldReset(context)) { _scrollController.reset(); } } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: _extent._currentSize, builder: (BuildContext context, double currentSize, Widget? child) => LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { _extent.availablePixels = widget.maxChildSize * constraints.biggest.height; final Widget sheet = FractionallySizedBox( heightFactor: currentSize, alignment: Alignment.bottomCenter, child: child, ); return widget.expand ? SizedBox.expand(child: sheet) : sheet; }, ), child: widget.builder(context, _scrollController), ); } @override void dispose() { if (widget.controller == null) { _extent.dispose(); } else { widget.controller!._detach(disposeExtent: true); } _scrollController.dispose(); super.dispose(); } void _replaceExtent(covariant DraggableScrollableSheet oldWidget) { final _DraggableSheetExtent previousExtent = _extent; _extent = previousExtent.copyWith( minSize: widget.minChildSize, maxSize: widget.maxChildSize, snap: widget.snap, snapSizes: _impliedSnapSizes(), snapAnimationDuration: widget.snapAnimationDuration, initialSize: widget.initialChildSize, shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, ); // Modify the existing scroll controller instead of replacing it so that // developers listening to the controller do not have to rebuild their listeners. _scrollController.extent = _extent; // If an external facing controller was provided, let it know that the // extent has been replaced. widget.controller?._onExtentReplaced(previousExtent); previousExtent.dispose(); if (widget.snap && (widget.snap != oldWidget.snap || widget.snapSizes != oldWidget.snapSizes) && _scrollController.hasClients) { // Trigger a snap in case snap or snapSizes has changed and there is a // scroll position currently attached. We put this in a post frame // callback so that `build` can update `_extent.availablePixels` before // this runs-we can't use the previous extent's available pixels as it may // have changed when the widget was updated. WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { for ( var index = 0; index < _scrollController.positions.length; index++ ) { final position = _scrollController.positions.elementAt(index) as _DraggableScrollableSheetScrollPosition; position.goBallistic(0); } }, debugLabel: 'DraggableScrollableSheet.snap'); } } String _snapSizeErrorMessage(int invalidIndex) { final List snapSizesWithIndicator = widget.snapSizes! .asMap() .keys .map((int index) { final snapSizeString = widget.snapSizes![index].toString(); if (index == invalidIndex) { return '>>> $snapSizeString <<<'; } return snapSizeString; }) .toList(); return "Invalid snapSize '${widget.snapSizes![invalidIndex]}' at index $invalidIndex of:\n" ' $snapSizesWithIndicator'; } } /// A [ScrollController] suitable for use in a [ScrollableWidgetBuilder] created /// by a [DraggableScrollableSheet]. /// /// If a [DraggableScrollableSheet] contains content that is exceeds the height /// of its container, this controller will allow the sheet to both be dragged to /// fill the container and then scroll the child content. /// /// See also: /// /// * [_DraggableScrollableSheetScrollPosition], which manages the positioning logic for /// this controller. /// * [PrimaryScrollController], which can be used to establish a /// [_DraggableScrollableSheetScrollController] as the primary controller for /// descendants. class _DraggableScrollableSheetScrollController extends ScrollController { _DraggableScrollableSheetScrollController({ required this.extent, double initialScrollOffset = 0.0, }) : _initialScrollOffset = initialScrollOffset; _DraggableSheetExtent extent; VoidCallback? onPositionDetached; @override double get initialScrollOffset => _initialScrollOffset; final double _initialScrollOffset; @override _DraggableScrollableSheetScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition, ) { return _DraggableScrollableSheetScrollPosition( physics: physics.applyTo(const AlwaysScrollableScrollPhysics()), context: context, oldPosition: oldPosition, getExtent: () => extent, initialPixels: _initialScrollOffset, ); } @override void debugFillDescription(List description) { super.debugFillDescription(description); description.add('extent: $extent'); } @override _DraggableScrollableSheetScrollPosition get position => super.position as _DraggableScrollableSheetScrollPosition; void reset() { extent._cancelActivity?.call(); extent.hasDragged = false; extent.hasChanged = false; // jumpTo can result in trying to replace semantics during build. // Just animate really fast. // Avoid doing it at all if the offset is already 0.0. if (offset != 0.0) { animateTo( 0.0, duration: const Duration(milliseconds: 1), curve: Curves.linear, ); } extent.updateSize( extent.initialSize, position.context.notificationContext!, ); } @override void detach(ScrollPosition position) { onPositionDetached?.call(); super.detach(position); } } /// A scroll position that manages scroll activities for /// [_DraggableScrollableSheetScrollController]. /// /// This class is a concrete subclass of [ScrollPosition] logic that handles a /// single [ScrollContext], such as a [Scrollable]. An instance of this class /// manages [ScrollActivity] instances, which changes the /// [_DraggableSheetExtent.currentSize] or visible content offset in the /// [Scrollable]'s [Viewport] /// /// See also: /// /// * [_DraggableScrollableSheetScrollController], which uses this as its [ScrollPosition]. class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleContext { _DraggableScrollableSheetScrollPosition({ required super.physics, required super.context, super.oldPosition, required this.getExtent, super.initialPixels, }); VoidCallback? _dragCancelCallback; final _DraggableSheetExtent Function() getExtent; final Set _ballisticControllers = {}; bool get listShouldScroll => pixels > 0.0 && extent.isAtMax; _DraggableSheetExtent get extent => getExtent(); @override void absorb(ScrollPosition other) { super.absorb(other); assert(_dragCancelCallback == null); if (other is! _DraggableScrollableSheetScrollPosition) { return; } if (other._dragCancelCallback != null) { _dragCancelCallback = other._dragCancelCallback; other._dragCancelCallback = null; } } @override void beginActivity(ScrollActivity? newActivity) { // Cancel the running ballistic simulations for (final AnimationController ballisticController in _ballisticControllers) { ballisticController.stop(); } super.beginActivity(newActivity); } @override void applyUserOffset(double delta) { if (!listShouldScroll && (!(extent.isAtMin || extent.isAtMax) || (extent.isAtMin && delta < 0) || (extent.isAtMax && delta > 0))) { extent.addPixelDelta(-delta, context.notificationContext!); } else { super.applyUserOffset(delta); } } // Checks if the sheet's current size is close to a snap size, returning the // snap size if so; returns null otherwise. double? _getCurrentSnapSize() { return extent.snapSizes.firstWhereOrNull((double snapSize) { return (extent.currentSize - snapSize).abs() <= extent.pixelsToSize(physics.toleranceFor(this).distance); }); } bool _isAtSnapSize() => _getCurrentSnapSize() != null; bool _shouldSnap() => extent.snap && extent.hasDragged && !_isAtSnapSize(); @override void dispose() { for (final AnimationController ballisticController in _ballisticControllers) { ballisticController.dispose(); } _ballisticControllers.clear(); super.dispose(); } @override void goBallistic(double velocity) { if ((velocity == 0.0 && !_shouldSnap()) || (velocity < 0.0 && listShouldScroll) || (velocity > 0.0 && extent.isAtMax)) { super.goBallistic(velocity); return; } // Scrollable expects that we will dispose of its current _dragCancelCallback _dragCancelCallback?.call(); _dragCancelCallback = null; late final Simulation simulation; if (extent.snap) { // Snap is enabled, simulate snapping instead of clamping scroll. simulation = _SnappingSimulation( position: extent.currentPixels, initialVelocity: velocity, pixelSnapSize: extent.pixelSnapSizes, snapAnimationDuration: extent.snapAnimationDuration, tolerance: physics.toleranceFor(this), ); } else { // The iOS bouncing simulation just isn't right here - once we delegate // the ballistic back to the ScrollView, it will use the right simulation. simulation = ClampingScrollSimulation( // Run the simulation in terms of pixels, not extent. position: extent.currentPixels, velocity: velocity, tolerance: physics.toleranceFor(this), ); } final ballisticController = AnimationController.unbounded( debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'), vsync: context.vsync, ); _ballisticControllers.add(ballisticController); double lastPosition = extent.currentPixels; void tick() { final double delta = ballisticController.value - lastPosition; lastPosition = ballisticController.value; extent.addPixelDelta(delta, context.notificationContext!); if ((velocity > 0 && extent.isAtMax) || (velocity < 0 && extent.isAtMin)) { // Make sure we pass along enough velocity to keep scrolling - otherwise // we just "bounce" off the top making it look like the list doesn't // have more to scroll. velocity = ballisticController.velocity + (physics.toleranceFor(this).velocity * ballisticController.velocity.sign); super.goBallistic(velocity); ballisticController.stop(); } else if (ballisticController.isCompleted) { // Update the extent value after the snap animation completes to // avoid rounding errors that could prevent the sheet from closing when // it reaches minSize. final double? snapSize = _getCurrentSnapSize(); if (snapSize != null) { extent.updateSize(snapSize, context.notificationContext!); } super.goBallistic(0); } } ballisticController ..addListener(tick) ..animateWith(simulation).whenCompleteOrCancel(() { if (_ballisticControllers.contains(ballisticController)) { _ballisticControllers.remove(ballisticController); ballisticController.dispose(); } }); } @override Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { // Save this so we can call it later if we have to [goBallistic] on our own. _dragCancelCallback = dragCancelCallback; return super.drag(details, dragCancelCallback); } } /// A widget that can notify a descendent [DraggableScrollableSheet] that it /// should reset its position to the initial state. /// /// The [Scaffold] uses this widget to notify a persistent bottom sheet that /// the user has tapped back if the sheet has started to cover more of the body /// than when at its initial position. This is important for users of assistive /// technology, where dragging may be difficult to communicate. /// /// This is just a wrapper on top of [DraggableScrollableController]. It is /// primarily useful for controlling a sheet in a part of the widget tree that /// the current code does not control (e.g. library code trying to affect a sheet /// in library users' code). Generally, it's easier to control the sheet /// directly by creating a controller and passing the controller to the sheet in /// its constructor (see [DraggableScrollableSheet.controller]). class DraggableScrollableActuator extends StatefulWidget { /// Creates a widget that can notify descendent [DraggableScrollableSheet]s /// to reset to their initial position. /// /// The [child] parameter is required. const DraggableScrollableActuator({super.key, required this.child}); /// This child's [DraggableScrollableSheet] descendant will be reset when the /// [reset] method is applied to a context that includes it. final Widget child; /// Notifies any descendant [DraggableScrollableSheet] that it should reset /// to its initial position. /// /// Returns `true` if a [DraggableScrollableActuator] is available and /// some [DraggableScrollableSheet] is listening for updates, `false` /// otherwise. static bool reset(BuildContext context) { final _InheritedResetNotifier? notifier = context .dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>(); return notifier?._sendReset() ?? false; } @override State createState() => _DraggableScrollableActuatorState(); } class _DraggableScrollableActuatorState extends State { final _ResetNotifier _notifier = _ResetNotifier(); @override Widget build(BuildContext context) { return _InheritedResetNotifier(notifier: _notifier, child: widget.child); } @override void dispose() { _notifier.dispose(); super.dispose(); } } /// A [ChangeNotifier] to use with [_InheritedResetNotifier] to notify /// descendants that they should reset to initial state. class _ResetNotifier extends ChangeNotifier { _ResetNotifier() { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } /// Whether someone called [sendReset] or not. /// /// This flag should be reset after checking it. bool _wasCalled = false; /// Fires a reset notification to descendants. /// /// Returns false if there are no listeners. bool sendReset() { if (!hasListeners) { return false; } _wasCalled = true; notifyListeners(); return true; } } class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> { /// Creates an [InheritedNotifier] that the [DraggableScrollableSheet] will /// listen to for an indication that it should reset itself back to [DraggableScrollableSheet.initialChildSize]. const _InheritedResetNotifier({ required super.child, required _ResetNotifier super.notifier, }); bool _sendReset() => notifier!.sendReset(); /// Specifies whether the [DraggableScrollableSheet] should reset to its /// initial position. /// /// Returns true if the notifier requested a reset, false otherwise. static bool shouldReset(BuildContext context) { final InheritedWidget? widget = context .dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>(); if (widget == null) { return false; } assert(widget is _InheritedResetNotifier); final inheritedNotifier = widget as _InheritedResetNotifier; final bool wasCalled = inheritedNotifier.notifier!._wasCalled; inheritedNotifier.notifier!._wasCalled = false; return wasCalled; } } class _SnappingSimulation extends Simulation { _SnappingSimulation({ required this.position, required double initialVelocity, required List pixelSnapSize, Duration? snapAnimationDuration, super.tolerance, }) { _pixelSnapSize = _getSnapSize(initialVelocity, pixelSnapSize); if (snapAnimationDuration != null && snapAnimationDuration.inMilliseconds > 0) { velocity = (_pixelSnapSize - position) * 1000 / snapAnimationDuration.inMilliseconds; } // Check the direction of the target instead of the sign of the velocity because // we may snap in the opposite direction of velocity if velocity is very low. else if (_pixelSnapSize < position) { velocity = math.min(-minimumSpeed, initialVelocity); } else { velocity = math.max(minimumSpeed, initialVelocity); } } final double position; late final double velocity; // A minimum speed to snap at. Used to ensure that the snapping animation // does not play too slowly. static const double minimumSpeed = 1600.0; late final double _pixelSnapSize; @override double dx(double time) { if (isDone(time)) { return 0; } return velocity; } @override bool isDone(double time) { return x(time) == _pixelSnapSize; } @override double x(double time) { final double newPosition = position + velocity * time; if ((velocity >= 0 && newPosition > _pixelSnapSize) || (velocity < 0 && newPosition < _pixelSnapSize)) { // We're passed the snap size, return it instead. return _pixelSnapSize; } return newPosition; } // Find the two closest snap sizes to the position. If the velocity is // non-zero, select the size in the velocity's direction. Otherwise, // the nearest snap size. double _getSnapSize(double initialVelocity, List pixelSnapSizes) { final int indexOfNextSize = pixelSnapSizes.indexWhere( (double size) => size >= position, ); if (indexOfNextSize == 0) { return pixelSnapSizes.first; } final double nextSize = pixelSnapSizes[indexOfNextSize]; // If already snapped - keep this as target size if (nextSize == position) { return nextSize; } final double previousSize = pixelSnapSizes[indexOfNextSize - 1]; if (initialVelocity.abs() <= tolerance.velocity) { // If velocity is zero, snap to the nearest snap size with the minimum velocity. if (position - previousSize < nextSize - position) { return previousSize; } else { return nextSize; } } // Snap forward or backward depending on current velocity. if (initialVelocity < 0.0) { return pixelSnapSizes[indexOfNextSize - 1]; } return pixelSnapSizes[indexOfNextSize]; } } ================================================ FILE: lib/common/widgets/flutter/layout_builder.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; /// An abstract superclass for widgets that defer their building until layout. /// /// Similar to the [Builder] widget except that the implementation calls the [builder] /// function at layout time and provides the [LayoutInfoType] that is required to /// configure the child widget subtree. /// /// This is useful when the child widget tree relies on information that are only /// available during layout, and doesn't depend on the child's intrinsic size. /// /// The [LayoutInfoType] should typically be immutable. The equality of the /// [LayoutInfoType] type is used by the implementation to avoid unnecessary /// rebuilds: if the new [LayoutInfoType] computed during layout is the same as /// (defined by `LayoutInfoType.==`) the previous [LayoutInfoType], the /// implementation will try to avoid calling the [builder] again unless /// [updateShouldRebuild] returns true. The corresponding [RenderObject] produced /// by this widget retains the most up-to-date [LayoutInfoType] for this purpose, /// which may keep a [LayoutInfoType] object in memory until the widget is removed /// from the tree. /// /// Subclasses must return a [RenderObject] that mixes in [RenderAbstractLayoutBuilderMixin]. abstract class AbstractLayoutBuilder extends RenderObjectWidget { /// Creates a widget that defers its building until layout. const AbstractLayoutBuilder({super.key}); /// Called at layout time to construct the widget tree. /// /// The builder must not return null. Widget Function(BuildContext context, LayoutInfoType layoutInfo) get builder; @override RenderObjectElement createElement() => _LayoutBuilderElement(this); /// Whether [builder] needs to be called again even if the layout constraints /// are the same. /// /// When this widget's configuration is updated, the [builder] callback most /// likely needs to be called to build this widget's child. However, /// subclasses may provide ways in which the widget can be updated without /// needing to rebuild the child. Such subclasses can use this method to tell /// the framework when the child widget should be rebuilt. /// /// When this method is called by the framework, the newly configured widget /// is asked if it requires a rebuild, and it is passed the old widget as a /// parameter. /// /// See also: /// /// * [State.setState] and [State.didUpdateWidget], which talk about widget /// configuration changes and how they're triggered. /// * [Element.update], the method that actually updates the widget's /// configuration. @protected bool updateShouldRebuild( covariant AbstractLayoutBuilder oldWidget, ) => true; @override RenderAbstractLayoutBuilderMixin createRenderObject( BuildContext context, ); // updateRenderObject is redundant with the logic in the LayoutBuilderElement below. } /// A specialized [AbstractLayoutBuilder] whose widget subtree depends on the /// incoming [ConstraintType] that will be imposed on the widget. /// /// {@template flutter.widgets.ConstrainedLayoutBuilder} /// The [builder] function is called in the following situations: /// /// * The first time the widget is laid out. /// * When the parent widget passes different layout constraints. /// * When the parent widget updates this widget and [updateShouldRebuild] returns `true`. /// * When the dependencies that the [builder] function subscribes to change. /// /// The [builder] function is _not_ called during layout if the parent passes /// the same constraints repeatedly. /// /// In the event that an ancestor skips the layout of this subtree so the /// constraints become outdated, the `builder` rebuilds with the last known /// constraints. /// {@endtemplate} abstract class ConstrainedLayoutBuilder extends AbstractLayoutBuilder { /// Creates a widget that defers its building until layout. const ConstrainedLayoutBuilder({super.key, required this.builder}); @override final Widget Function(BuildContext context, ConstraintType constraints) builder; } class _LayoutBuilderElement extends RenderObjectElement { _LayoutBuilderElement(AbstractLayoutBuilder super.widget); @override RenderAbstractLayoutBuilderMixin get renderObject => super.renderObject as RenderAbstractLayoutBuilderMixin; Element? _child; // @override // BuildScope get buildScope => _buildScope; // late final BuildScope _buildScope = BuildScope( // scheduleRebuild: _scheduleRebuild, // ); // To schedule a rebuild, markNeedsLayout needs to be called on this Element's // render object (as the rebuilding is done in its performLayout call). However, // the render tree should typically be kept clean during the postFrameCallbacks // and the idle phase, so the layout data can be safely read. // bool _deferredCallbackScheduled = false; // void _scheduleRebuild() { // if (_deferredCallbackScheduled) { // return; // } // final bool deferMarkNeedsLayout = // switch (SchedulerBinding.instance.schedulerPhase) { // SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, // SchedulerPhase.transientCallbacks || // SchedulerPhase.midFrameMicrotasks || // SchedulerPhase.persistentCallbacks => false, // }; // if (!deferMarkNeedsLayout) { // renderObject.scheduleLayoutCallback(); // return; // } // _deferredCallbackScheduled = true; // SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); // } // void _frameCallback(Duration timestamp) { // _deferredCallbackScheduled = false; // // This method is only called when the render tree is stable, if the Element // // is deactivated it will never be reincorporated back to the tree. // if (mounted) { // renderObject.scheduleLayoutCallback(); // } // } @override void visitChildren(ElementVisitor visitor) { if (_child != null) { visitor(_child!); } } @override void forgetChild(Element child) { assert(child == _child); _child = null; super.forgetChild(child); } @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); // Creates the renderObject. renderObject._updateCallback(_rebuildWithConstraints); } @override void update(AbstractLayoutBuilder newWidget) { assert(widget != newWidget); final oldWidget = widget as AbstractLayoutBuilder; super.update(newWidget); assert(widget == newWidget); renderObject._updateCallback(_rebuildWithConstraints); if (newWidget.updateShouldRebuild(oldWidget)) { _needsBuild = true; renderObject.scheduleLayoutCallback(); } } @override void markNeedsBuild() { // Calling super.markNeedsBuild is not needed. This Element does not need // to performRebuild since this call already does what performRebuild does, // So the element is clean as soon as this method returns and does not have // to be added to the dirty list or marked as dirty. renderObject.scheduleLayoutCallback(); _needsBuild = true; } @override void performRebuild() { // This gets called if markNeedsBuild() is called on us. // That might happen if, e.g., our builder uses Inherited widgets. // Force the callback to be called, even if the layout constraints are the // same. This is because that callback may depend on the updated widget // configuration, or an inherited widget. renderObject.scheduleLayoutCallback(); _needsBuild = true; super .performRebuild(); // Calls widget.updateRenderObject (a no-op in this case). } @override void unmount() { renderObject._callback = null; super.unmount(); } // The LayoutInfoType that was used to invoke the layout callback with last time, // during layout. The `_previousLayoutInfo` value is compared to the new one // to determine whether [LayoutBuilderBase.builder] needs to be called. LayoutInfoType? _previousLayoutInfo; bool _needsBuild = true; void _rebuildWithConstraints(Constraints _) { final LayoutInfoType layoutInfo = renderObject.layoutInfo; @pragma('vm:notify-debugger-on-exception') void updateChildCallback() { Widget built; try { assert(layoutInfo == renderObject.layoutInfo); built = (widget as AbstractLayoutBuilder).builder( this, layoutInfo, ); debugWidgetBuilderValue(widget, built); } catch (e, stack) { built = ErrorWidget.builder( _reportException( ErrorDescription('building $widget'), e, stack, informationCollector: () => [ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), ], ), ); } try { _child = updateChild(_child, built, null); assert(_child != null); } catch (e, stack) { built = ErrorWidget.builder( _reportException( ErrorDescription('building $widget'), e, stack, informationCollector: () => [ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), ], ), ); _child = updateChild(null, built, slot); } finally { _needsBuild = false; _previousLayoutInfo = layoutInfo; } } final VoidCallback? callback = _needsBuild || (layoutInfo != _previousLayoutInfo) ? updateChildCallback : null; owner!.buildScope(this, callback); } @override void insertRenderObjectChild(RenderObject child, Object? slot) { final RenderObjectWithChildMixin renderObject = this.renderObject; assert(slot == null); assert(renderObject.debugValidateChild(child)); renderObject.child = child; assert(renderObject == this.renderObject); } @override void moveRenderObjectChild( RenderObject child, Object? oldSlot, Object? newSlot, ) { assert(false); } @override void removeRenderObjectChild(RenderObject child, Object? slot) { final RenderAbstractLayoutBuilderMixin renderObject = this.renderObject; assert(renderObject.child == child); renderObject.child = null; assert(renderObject == this.renderObject); } } /// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with /// the the same `LayoutInfoType`. /// /// Provides a [layoutCallback] implementation which, if needed, invokes /// [AbstractLayoutBuilder]'s builder callback. /// /// Implementers can override the [layoutInfo] implementation with a value /// that is safe to access in [layoutCallback], which is called in /// [performLayout]. The default [layoutInfo] returns the incoming /// [Constraints]. /// /// This mixin replaces [RenderConstrainedLayoutBuilder]. mixin RenderAbstractLayoutBuilderMixin< LayoutInfoType, ChildType extends RenderObject > on RenderObjectWithChildMixin, RenderObjectWithLayoutCallbackMixin { LayoutCallback? _callback; /// Change the layout callback. void _updateCallback(LayoutCallback value) { if (value == _callback) { return; } _callback = value; scheduleLayoutCallback(); } /// Invokes the builder callback supplied via [AbstractLayoutBuilder] and /// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed. /// /// No further work will be done if [layoutInfo] has not changed since the last /// time this method was called, and [AbstractLayoutBuilder.updateShouldRebuild] /// returned `false` when the widget was rebuilt. /// /// This method should typically be called as soon as possible in the class's /// [performLayout] implementation, before any layout work is done. @visibleForOverriding @override void layoutCallback() => _callback!(constraints); /// The information to invoke the [AbstractLayoutBuilder.builder] callback with. /// /// This is typically the information that are only made available in /// [performLayout], which is inaccessible for regular [Builder] widget, /// such as the incoming [Constraints], which are the default value. @protected LayoutInfoType get layoutInfo => constraints as LayoutInfoType; } /// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with /// the the same `LayoutInfoType`. /// /// Use [RenderAbstractLayoutBuilderMixin] instead, which replaces this mixin. typedef RenderConstrainedLayoutBuilder< LayoutInfoType, ChildType extends RenderObject > = RenderAbstractLayoutBuilderMixin; /// Builds a widget tree that can depend on the parent widget's size. /// /// Similar to the [Builder] widget except that the framework calls the [builder] /// function at layout time and provides the parent widget's constraints. This /// is useful when the parent constrains the child's size and doesn't depend on /// the child's intrinsic size. The [LayoutBuilder]'s final size will match its /// child's size. /// /// {@macro flutter.widgets.ConstrainedLayoutBuilder} /// /// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw} /// /// If the child should be smaller than the parent, consider wrapping the child /// in an [Align] widget. If the child might want to be bigger, consider /// wrapping it in a [SingleChildScrollView] or [OverflowBox]. /// /// {@tool dartpad} /// This example uses a [LayoutBuilder] to build a different widget depending on the available width. Resize the /// DartPad window to see [LayoutBuilder] in action! /// /// ** See code in examples/api/lib/widgets/layout_builder/layout_builder.0.dart ** /// {@end-tool} /// /// See also: /// /// * [SliverLayoutBuilder], the sliver counterpart of this widget. /// * [Builder], which calls a `builder` function at build time. /// * [StatefulBuilder], which passes its `builder` function a `setState` callback. /// * [CustomSingleChildLayout], which positions its child during layout. /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class LayoutBuilder extends ConstrainedLayoutBuilder { /// Creates a widget that defers its building until layout. const LayoutBuilder({super.key, required super.builder}); @override RenderAbstractLayoutBuilderMixin createRenderObject( BuildContext context, ) => _RenderLayoutBuilder(); } class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin, RenderObjectWithLayoutCallbackMixin, RenderAbstractLayoutBuilderMixin { @override double computeMinIntrinsicWidth(double height) { assert(_debugThrowIfNotCheckingIntrinsics()); return 0.0; } @override double computeMaxIntrinsicWidth(double height) { assert(_debugThrowIfNotCheckingIntrinsics()); return 0.0; } @override double computeMinIntrinsicHeight(double width) { assert(_debugThrowIfNotCheckingIntrinsics()); return 0.0; } @override double computeMaxIntrinsicHeight(double width) { assert(_debugThrowIfNotCheckingIntrinsics()); return 0.0; } @override Size computeDryLayout(BoxConstraints constraints) { assert( debugCannotComputeDryLayout( reason: 'Calculating the dry layout would require running the layout callback ' 'speculatively, which might mutate the live render object tree.', ), ); return Size.zero; } @override double? computeDryBaseline( BoxConstraints constraints, TextBaseline baseline, ) { assert( debugCannotComputeDryLayout( reason: 'Calculating the dry baseline would require running the layout callback ' 'speculatively, which might mutate the live render object tree.', ), ); return null; } @override void performLayout() { final BoxConstraints constraints = this.constraints; runLayoutCallback(); if (child != null) { child!.layout(constraints, parentUsesSize: true); size = constraints.constrain(child!.size); } else { size = constraints.biggest; } } @override double? computeDistanceToActualBaseline(TextBaseline baseline) { return child?.getDistanceToActualBaseline(baseline) ?? super.computeDistanceToActualBaseline(baseline); } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return child?.hitTest(result, position: position) ?? false; } @override void paint(PaintingContext context, Offset offset) { if (child != null) { context.paintChild(child!, offset); } } bool _debugThrowIfNotCheckingIntrinsics() { assert(() { if (!RenderObject.debugCheckingIntrinsics) { throw FlutterError( 'LayoutBuilder does not support returning intrinsic dimensions.\n' 'Calculating the intrinsic dimensions would require running the layout ' 'callback speculatively, which might mutate the live render object tree.', ); } return true; }()); return true; } } FlutterErrorDetails _reportException( DiagnosticsNode context, Object exception, StackTrace stack, { InformationCollector? informationCollector, }) { final details = FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: context, informationCollector: informationCollector, ); FlutterError.reportError(details); return details; } ================================================ FILE: lib/common/widgets/flutter/list_tile.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'card.dart'; /// @docImport 'checkbox.dart'; /// @docImport 'checkbox_list_tile.dart'; /// @docImport 'circle_avatar.dart'; /// @docImport 'drawer.dart'; /// @docImport 'expansion_tile.dart'; /// @docImport 'material.dart'; /// @docImport 'radio.dart'; /// @docImport 'radio_list_tile.dart'; /// @docImport 'scaffold.dart'; /// @docImport 'switch.dart'; /// @docImport 'switch_list_tile.dart'; library; import 'dart:math' as math; import 'package:flutter/material.dart' hide ListTile; import 'package:flutter/rendering.dart'; // Examples can assume: // int _act = 1; typedef _Sizes = ({ double titleY, BoxConstraints textConstraints, Size tileSize, }); typedef _PositionChild = void Function(RenderBox child, Offset offset); /// Defines how [ListTile.leading] and [ListTile.trailing] are /// vertically aligned relative to the [ListTile]'s titles /// ([ListTile.title] and [ListTile.subtitle]). /// /// See also: /// /// * [ListTile.titleAlignment], to configure the title alignment for an /// individual [ListTile]. /// * [ListTileThemeData.titleAlignment], to configure the title alignment /// for all of the [ListTile]s under a [ListTileTheme]. /// * [ThemeData.listTileTheme], to configure the [ListTileTheme] /// for an entire app. extension on ListTileTitleAlignment { // If isLeading is true the y offset is for the leading widget, otherwise it's // for the trailing child. double _yOffsetFor( double childHeight, double tileHeight, _RenderListTile listTile, bool isLeading, ) { return switch (this) { ListTileTitleAlignment.threeLine => listTile.isThreeLine ? ListTileTitleAlignment.top._yOffsetFor( childHeight, tileHeight, listTile, isLeading, ) : ListTileTitleAlignment.center._yOffsetFor( childHeight, tileHeight, listTile, isLeading, ), // This attempts to implement the redlines for the vertical position of the // leading and trailing icons on the spec page: // https://m2.material.io/components/lists#specs // // For large tiles (> 72dp), both leading and trailing controls should be // a fixed distance from top. As per guidelines this is set to 16dp. ListTileTitleAlignment.titleHeight when tileHeight > 72.0 => 16.0, // For smaller tiles, trailing should always be centered. Leading can be // centered or closer to the top. It should never be further than 16dp // to the top. ListTileTitleAlignment.titleHeight => isLeading ? math.min((tileHeight - childHeight) / 2.0, 16.0) : (tileHeight - childHeight) / 2.0, ListTileTitleAlignment.top => listTile.minVerticalPadding, ListTileTitleAlignment.center => (tileHeight - childHeight) / 2.0, ListTileTitleAlignment.bottom => tileHeight - childHeight - listTile.minVerticalPadding, }; } } /// A single fixed-height row that typically contains some text as well as /// a leading or trailing icon. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=l8dj0yPBvgQ} /// /// A list tile contains one to three lines of text optionally flanked by icons or /// other widgets, such as check boxes. The icons (or other widgets) for the /// tile are defined with the [leading] and [trailing] parameters. The first /// line of text is not optional and is specified with [title]. The value of /// [subtitle], which _is_ optional, will occupy the space allocated for an /// additional line of text, or two lines if [isThreeLine] is true. If [dense] /// is true then the overall height of this tile and the size of the /// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced. /// /// It is the responsibility of the caller to ensure that [title] does not wrap, /// and to ensure that [subtitle] doesn't wrap (if [isThreeLine] is false) or /// wraps to two lines (if it is true). /// /// The heights of the [leading] and [trailing] widgets are constrained /// according to the /// [Material spec](https://material.io/design/components/lists.html). /// An exception is made for one-line ListTiles for accessibility. Please /// see the example below to see how to adhere to both Material spec and /// accessibility requirements. /// /// The [leading] and [trailing] widgets can expand as far as they wish /// horizontally, so ensure that they are properly constrained. /// /// List tiles are typically used in [ListView]s, or arranged in [Column]s in /// [Drawer]s and [Card]s. /// /// This widget requires a [Material] widget ancestor in the tree to paint /// itself on, which is typically provided by the app's [Scaffold]. /// The [tileColor], [selectedTileColor], [focusColor], and [hoverColor] /// are not painted by the [ListTile] itself but by the [Material] widget /// ancestor. In this case, one can wrap a [Material] widget around the /// [ListTile], e.g.: /// /// {@tool snippet} /// ```dart /// const ColoredBox( /// color: Colors.green, /// child: Material( /// child: ListTile( /// title: Text('ListTile with red background'), /// tileColor: Colors.red, /// ), /// ), /// ) /// ``` /// {@end-tool} /// /// ## Performance considerations when wrapping [ListTile] with [Material] /// /// Wrapping a large number of [ListTile]s individually with [Material]s /// is expensive. Consider only wrapping the [ListTile]s that require it /// or include a common [Material] ancestor where possible. /// /// [ListTile] must be wrapped in a [Material] widget to animate [tileColor], /// [selectedTileColor], [focusColor], and [hoverColor] as these colors /// are not drawn by the list tile itself but by the material widget ancestor. /// /// {@tool dartpad} /// This example showcases how [ListTile] needs to be wrapped in a [Material] /// widget to animate colors. /// /// ** See code in examples/api/lib/material/list_tile/list_tile.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This example uses a [ListView] to demonstrate different configurations of /// [ListTile]s in [Card]s. /// /// ![Different variations of ListTile](https://flutter.github.io/assets-for-api-docs/assets/material/list_tile.png) /// /// ** See code in examples/api/lib/material/list_tile/list_tile.1.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This sample shows the creation of a [ListTile] using [ThemeData.useMaterial3] flag, /// as described in: https://m3.material.io/components/lists/overview. /// /// ** See code in examples/api/lib/material/list_tile/list_tile.2.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This sample shows [ListTile]'s [textColor] and [iconColor] can use /// [WidgetStateColor] color to change the color of the text and icon /// when the [ListTile] is enabled, selected, or disabled. /// /// ** See code in examples/api/lib/material/list_tile/list_tile.3.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This sample shows [ListTile.titleAlignment] can be used to configure the /// [leading] and [trailing] widgets alignment relative to the [title] and /// [subtitle] widgets. /// /// ** See code in examples/api/lib/material/list_tile/list_tile.4.dart ** /// {@end-tool} /// /// {@tool snippet} /// To use a [ListTile] within a [Row], it needs to be wrapped in an /// [Expanded] widget. [ListTile] requires fixed width constraints, /// whereas a [Row] does not constrain its children. /// /// ```dart /// const Row( /// children: [ /// Expanded( /// child: ListTile( /// leading: FlutterLogo(), /// title: Text('These ListTiles are expanded '), /// ), /// ), /// Expanded( /// child: ListTile( /// trailing: FlutterLogo(), /// title: Text('to fill the available space.'), /// ), /// ), /// ], /// ) /// ``` /// {@end-tool} /// {@tool snippet} /// /// Tiles can be much more elaborate. Here is a tile which can be tapped, but /// which is disabled when the `_act` variable is not 2. When the tile is /// tapped, the whole row has an ink splash effect (see [InkWell]). /// /// ```dart /// ListTile( /// leading: const Icon(Icons.flight_land), /// title: const Text("Trix's airplane"), /// subtitle: _act != 2 ? const Text('The airplane is only in Act II.') : null, /// enabled: _act == 2, /// onTap: () { /* react to the tile being tapped */ } /// ) /// ``` /// {@end-tool} /// /// To be accessible, tappable [leading] and [trailing] widgets have to /// be at least 48x48 in size. However, to adhere to the Material spec, /// [trailing] and [leading] widgets in one-line ListTiles should visually be /// at most 32 ([dense]: true) or 40 ([dense]: false) in height, which may /// conflict with the accessibility requirement. /// /// For this reason, a one-line ListTile allows the height of [leading] /// and [trailing] widgets to be constrained by the height of the ListTile. /// This allows for the creation of tappable [leading] and [trailing] widgets /// that are large enough, but it is up to the developer to ensure that /// their widgets follow the Material spec. /// /// {@tool snippet} /// /// Here is an example of a one-line, non-[dense] ListTile with a /// tappable leading widget that adheres to accessibility requirements and /// the Material spec. To adjust the use case below for a one-line, [dense] /// ListTile, adjust the vertical padding to 8.0. /// /// ```dart /// ListTile( /// leading: GestureDetector( /// behavior: HitTestBehavior.translucent, /// onTap: () {}, /// child: Container( /// width: 48, /// height: 48, /// padding: const EdgeInsets.symmetric(vertical: 4.0), /// alignment: Alignment.center, /// child: const CircleAvatar(), /// ), /// ), /// title: const Text('title'), /// dense: false, /// ) /// ``` /// {@end-tool} /// /// ## The ListTile layout isn't exactly what I want /// /// If the way ListTile pads and positions its elements isn't quite what /// you're looking for, it's easy to create custom list items with a /// combination of other widgets, such as [Row]s and [Column]s. /// /// {@tool dartpad} /// Here is an example of a custom list item that resembles a YouTube-related /// video list item created with [Expanded] and [Container] widgets. /// /// ** See code in examples/api/lib/material/list_tile/custom_list_item.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// Here is an example of an article list item with multiline titles and /// subtitles. It utilizes [Row]s and [Column]s, as well as [Expanded] and /// [AspectRatio] widgets to organize its layout. /// /// ** See code in examples/api/lib/material/list_tile/custom_list_item.1.dart ** /// {@end-tool} /// /// See also: /// /// * [ListTileTheme], which defines visual properties for [ListTile]s. /// * [ListView], which can display an arbitrary number of [ListTile]s /// in a scrolling list. /// * [CircleAvatar], which shows an icon representing a person and is often /// used as the [leading] element of a ListTile. /// * [Card], which can be used with [Column] to show a few [ListTile]s. /// * [Divider], which can be used to separate [ListTile]s. /// * [ListTile.divideTiles], a utility for inserting [Divider]s in between [ListTile]s. /// * [CheckboxListTile], [RadioListTile], and [SwitchListTile], widgets /// that combine [ListTile] with other controls. /// * Material 3 [ListTile] specifications are referenced from /// and Material 2 [ListTile] specifications are referenced from /// * Cookbook: [Use lists](https://docs.flutter.dev/cookbook/lists/basic-list) /// * Cookbook: [Implement swipe to dismiss](https://docs.flutter.dev/cookbook/gestures/dismissible) class ListTile extends StatelessWidget { /// Creates a list tile. /// /// If [isThreeLine] is true, then [subtitle] must not be null. /// /// Requires one of its ancestors to be a [Material] widget. const ListTile({ super.key, this.safeArea = false, this.leading, this.title, this.subtitle, this.trailing, this.isThreeLine, this.dense, this.visualDensity, this.shape, this.style, this.selectedColor, this.iconColor, this.textColor, this.titleTextStyle, this.subtitleTextStyle, this.leadingAndTrailingTextStyle, this.contentPadding, this.enabled = true, this.onTap, this.onTapUp, this.onLongPress, this.onSecondaryTap, this.onSecondaryTapUp, this.onFocusChange, this.mouseCursor, this.selected = false, this.focusColor, this.hoverColor, this.splashColor, this.focusNode, this.autofocus = false, this.tileColor, this.selectedTileColor, this.enableFeedback, this.horizontalTitleGap, this.minVerticalPadding, this.minLeadingWidth, this.minTileHeight, this.titleAlignment, this.internalAddSemanticForOnTap = true, this.statesController, }) : assert(isThreeLine != true || subtitle != null); final bool safeArea; /// A widget to display before the title. /// /// Typically an [Icon] or a [CircleAvatar] widget. final Widget? leading; /// The primary content of the list tile. /// /// Typically a [Text] widget. /// /// This should not wrap. To enforce the single line limit, use /// [Text.maxLines]. final Widget? title; /// Additional content displayed below the title. /// /// Typically a [Text] widget. /// /// If [isThreeLine] is false, this should not wrap. /// /// If [isThreeLine] is true, this should be configured to take a maximum of /// two lines. For example, you can use [Text.maxLines] to enforce the number /// of lines. /// /// The subtitle's default [TextStyle] depends on [TextTheme.bodyMedium] except /// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled] /// and [selected]. /// /// When [enabled] is false, the text color is set to [ThemeData.disabledColor]. /// /// When [selected] is false, the text color is set to [ListTileTheme.textColor] /// if it's not null and to [TextTheme.bodySmall]'s color if [ListTileTheme.textColor] /// is null. final Widget? subtitle; /// A widget to display after the title. /// /// Typically an [Icon] widget. /// /// To show right-aligned metadata (assuming left-to-right reading order; /// left-aligned for right-to-left reading order), consider using a [Row] with /// [CrossAxisAlignment.baseline] alignment whose first item is [Expanded] and /// whose second child is the metadata text, instead of using the [trailing] /// property. final Widget? trailing; /// Whether this list tile is intended to display three lines of text. /// /// If true, then [subtitle] must be non-null (since it is expected to give /// the second and third lines of text). /// /// If false, the list tile is treated as having one line if the subtitle is /// null and treated as having two lines if the subtitle is non-null. /// /// When using a [Text] widget for [title] and [subtitle], you can enforce /// line limits using [Text.maxLines]. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final bool? isThreeLine; /// {@template flutter.material.ListTile.dense} /// Whether this list tile is part of a vertically dense list. /// /// If this property is null then its value is based on [ListTileTheme.dense]. /// /// Dense list tiles default to a smaller height. /// /// It is not recommended to set [dense] to true when [ThemeData.useMaterial3] is true. /// {@endtemplate} final bool? dense; /// Defines how compact the list tile's layout will be. /// /// {@macro flutter.material.themedata.visualDensity} /// /// See also: /// /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all /// widgets within a [Theme]. final VisualDensity? visualDensity; /// {@template flutter.material.ListTile.shape} /// Defines the tile's [InkWell.customBorder] and [Ink.decoration] shape. /// {@endtemplate} /// /// If this property is null then [ListTileThemeData.shape] is used. If that /// is also null then a rectangular [Border] will be used. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final ShapeBorder? shape; /// Defines the color used for icons and text when the list tile is selected. /// /// If this property is null then [ListTileThemeData.selectedColor] /// is used. If that is also null then [ColorScheme.primary] is used. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final Color? selectedColor; /// Defines the default color for [leading] and [trailing] icons. /// /// If this property is null and [selected] is false then [ListTileThemeData.iconColor] /// is used. If that is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant] /// is used, otherwise if [ThemeData.brightness] is [Brightness.light], [Colors.black54] is used, /// and if [ThemeData.brightness] is [Brightness.dark], the value is null. /// /// If this property is null and [selected] is true then [ListTileThemeData.selectedColor] /// is used. If that is also null then [ColorScheme.primary] is used. /// /// If this color is a [WidgetStateColor] it will be resolved against /// [WidgetState.selected] and [WidgetState.disabled] states. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final Color? iconColor; /// Defines the text color for the [title], [subtitle], [leading], and [trailing]. /// /// If this property is null and [selected] is false then [ListTileThemeData.textColor] /// is used. If that is also null then default text color is used for the [title], [subtitle] /// [leading], and [trailing]. Except for [subtitle], if [ThemeData.useMaterial3] is false, /// [TextTheme.bodySmall] is used. /// /// If this property is null and [selected] is true then [ListTileThemeData.selectedColor] /// is used. If that is also null then [ColorScheme.primary] is used. /// /// If this color is a [WidgetStateColor] it will be resolved against /// [WidgetState.selected] and [WidgetState.disabled] states. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final Color? textColor; /// The text style for ListTile's [title]. /// /// If this property is null, then [ListTileThemeData.titleTextStyle] is used. /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.bodyLarge] /// with [ColorScheme.onSurface] will be used. Otherwise, If ListTile style is /// [ListTileStyle.list], [TextTheme.titleMedium] will be used and if ListTile style /// is [ListTileStyle.drawer], [TextTheme.bodyLarge] will be used. final TextStyle? titleTextStyle; /// The text style for ListTile's [subtitle]. /// /// If this property is null, then [ListTileThemeData.subtitleTextStyle] is used. /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.bodyMedium] /// with [ColorScheme.onSurfaceVariant] will be used, otherwise [TextTheme.bodyMedium] /// with [TextTheme.bodySmall] color will be used. final TextStyle? subtitleTextStyle; /// The text style for ListTile's [leading] and [trailing]. /// /// If this property is null, then [ListTileThemeData.leadingAndTrailingTextStyle] is used. /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.labelSmall] /// with [ColorScheme.onSurfaceVariant] will be used, otherwise [TextTheme.bodyMedium] /// will be used. final TextStyle? leadingAndTrailingTextStyle; /// Defines the font used for the [title]. /// /// If this property is null then [ListTileThemeData.style] is used. If that /// is also null then [ListTileStyle.list] is used. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final ListTileStyle? style; /// The tile's internal padding. /// /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], and [trailing] widgets. /// /// If this property is null, then [ListTileThemeData.contentPadding] is used. If that is also /// null and [ThemeData.useMaterial3] is true, then a default value of /// `EdgeInsetsDirectional.only(start: 16.0, end: 24.0)` will be used. Otherwise, a default value /// of `EdgeInsets.symmetric(horizontal: 16.0)` will be used. final EdgeInsetsGeometry? contentPadding; /// Whether this list tile is interactive. /// /// If false, this list tile is styled with the disabled color from the /// current [Theme] and the [onTap] and [onLongPress] callbacks are /// inoperative. final bool enabled; /// Called when the user taps this list tile. /// /// Inoperative if [enabled] is false. final GestureTapCallback? onTap; final GestureTapUpCallback? onTapUp; /// Called when the user long-presses on this list tile. /// /// Inoperative if [enabled] is false. final GestureLongPressCallback? onLongPress; final GestureTapCallback? onSecondaryTap; final GestureTapUpCallback? onSecondaryTapUp; /// {@macro flutter.material.inkwell.onFocusChange} final ValueChanged? onFocusChange; /// {@template flutter.material.ListTile.mouseCursor} /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// /// If [mouseCursor] is a [WidgetStateMouseCursor], /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// /// * [WidgetState.selected]. /// * [WidgetState.disabled]. /// {@endtemplate} /// /// If null, then the value of [ListTileThemeData.mouseCursor] is used. If /// that is also null, then [WidgetStateMouseCursor.clickable] is used. final MouseCursor? mouseCursor; /// If this tile is also [enabled] then icons and text are rendered with the same color. /// /// By default the selected color is the theme's primary color. The selected color /// can be overridden with a [ListTileTheme]. /// /// {@tool dartpad} /// Here is an example of using a [StatefulWidget] to keep track of the /// selected index, and using that to set the [selected] property on the /// corresponding [ListTile]. /// /// ** See code in examples/api/lib/material/list_tile/list_tile.selected.0.dart ** /// {@end-tool} final bool selected; /// The color for the tile's [Material] when it has the input focus. final Color? focusColor; /// The color for the tile's [Material] when a pointer is hovering over it. final Color? hoverColor; /// The color of splash for the tile's [Material]. final Color? splashColor; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// {@template flutter.material.ListTile.tileColor} /// Defines the background color of `ListTile` when [selected] is false. /// /// If this property is null and [selected] is false then [ListTileThemeData.tileColor] /// is used. If that is also null and [selected] is true, [selectedTileColor] is used. /// When that is also null, the [ListTileTheme.selectedTileColor] is used, otherwise /// [Colors.transparent] is used. /// /// {@endtemplate} final Color? tileColor; /// Defines the background color of `ListTile` when [selected] is true. /// /// When the value if null, the [selectedTileColor] is set to [ListTileTheme.selectedTileColor] /// if it's not null and to [Colors.transparent] if it's null. final Color? selectedTileColor; /// {@template flutter.material.ListTile.enableFeedback} /// Whether detected gestures should provide acoustic and/or haptic feedback. /// /// For example, on Android a tap will produce a clicking sound and a /// long-press will produce a short vibration, when feedback is enabled. /// /// When null, the default value is true. /// {@endtemplate} /// /// See also: /// /// * [Feedback] for providing platform-specific feedback to certain actions. final bool? enableFeedback; /// The horizontal gap between the titles and the leading/trailing widgets. /// /// If null, then the value of [ListTileTheme.horizontalTitleGap] is used. If /// that is also null, then a default value of 16 is used. final double? horizontalTitleGap; /// The minimum padding on the top and bottom of the title and subtitle widgets. /// /// If null, then the value of [ListTileTheme.minVerticalPadding] is used. If /// that is also null, then a default value of 4 is used. final double? minVerticalPadding; /// The minimum width allocated for the [ListTile.leading] widget. /// /// If null, then the value of [ListTileTheme.minLeadingWidth] is used. If /// that is also null, then a default value of 40 is used. final double? minLeadingWidth; /// {@template flutter.material.ListTile.minTileHeight} /// The minimum height allocated for the [ListTile] widget. /// /// If this is null, default tile heights are 56.0, 72.0, and 88.0 for one, /// two, and three lines of text respectively. If `isDense` is true, these /// defaults are changed to 48.0, 64.0, and 76.0. A visual density value or /// a large title will also adjust the default tile heights. /// {@endtemplate} final double? minTileHeight; /// Defines how [ListTile.leading] and [ListTile.trailing] are /// vertically aligned relative to the [ListTile]'s titles /// ([ListTile.title] and [ListTile.subtitle]). /// /// If this property is null then [ListTileThemeData.titleAlignment] /// is used. If that is also null then [ListTileTitleAlignment.threeLine] /// is used. /// /// See also: /// /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s /// [ListTileThemeData]. final ListTileTitleAlignment? titleAlignment; /// Whether to add button:true to the semantics if onTap is provided. /// This is a temporary flag to help changing the behavior of ListTile onTap semantics. /// // TODO(hangyujin): Remove this flag after fixing related g3 tests and flipping // the default value to true. final bool internalAddSemanticForOnTap; /// {@macro flutter.material.inkwell.statesController} final WidgetStatesController? statesController; /// Add a one pixel border in between each tile. If color isn't specified the /// [ThemeData.dividerColor] of the context's [Theme] is used. /// /// See also: /// /// * [Divider], which you can use to obtain this effect manually. static Iterable divideTiles({ BuildContext? context, required Iterable tiles, Color? color, }) { assert(color != null || context != null); tiles = tiles.toList(); if (tiles.isEmpty || tiles.length == 1) { return tiles; } Widget wrapTile(Widget tile) { return DecoratedBox( position: DecorationPosition.foreground, decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, color: color), ), ), child: tile, ); } return [...tiles.take(tiles.length - 1).map(wrapTile), tiles.last]; } bool _isDenseLayout(ThemeData theme, ListTileThemeData tileTheme) { return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false; } Color _tileBackgroundColor( ThemeData theme, ListTileThemeData tileTheme, ListTileThemeData defaults, ) { final Color? color = selected ? selectedTileColor ?? tileTheme.selectedTileColor ?? theme.listTileTheme.selectedTileColor : tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor; return color ?? defaults.tileColor!; } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData theme = Theme.of(context); final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context); final ListTileThemeData tileTheme = ListTileTheme.of(context); final ListTileStyle listTileStyle = style ?? tileTheme.style ?? theme.listTileTheme.style ?? ListTileStyle.list; final ListTileThemeData defaults = theme.useMaterial3 ? _LisTileDefaultsM3(context) : _LisTileDefaultsM2(context, listTileStyle); final Set states = { if (!enabled) WidgetState.disabled, if (selected) WidgetState.selected, }; Color? resolveColor( Color? explicitColor, Color? selectedColor, Color? enabledColor, [ Color? disabledColor, ]) { return _IndividualOverrides( explicitColor: explicitColor, selectedColor: selectedColor, enabledColor: enabledColor, disabledColor: disabledColor, ).resolve(states); } Color? effectiveIconColor = resolveColor(iconColor, selectedColor, iconColor) ?? resolveColor( tileTheme.iconColor, tileTheme.selectedColor, tileTheme.iconColor, ) ?? resolveColor( theme.listTileTheme.iconColor, theme.listTileTheme.selectedColor, theme.listTileTheme.iconColor, ); final Color? defaultEffectiveIconColor = resolveColor( defaults.iconColor, defaults.selectedColor, defaults.iconColor, theme.disabledColor, ); final Color? effectiveIconButtonColor = effectiveIconColor ?? iconButtonTheme.style?.foregroundColor?.resolve(states) ?? defaultEffectiveIconColor; effectiveIconColor ??= defaultEffectiveIconColor; final Color? effectiveColor = resolveColor(textColor, selectedColor, textColor) ?? resolveColor( tileTheme.textColor, tileTheme.selectedColor, tileTheme.textColor, ) ?? resolveColor( theme.listTileTheme.textColor, theme.listTileTheme.selectedColor, theme.listTileTheme.textColor, ) ?? resolveColor( defaults.textColor, defaults.selectedColor, defaults.textColor, theme.disabledColor, ); final IconThemeData iconThemeData = IconThemeData( color: effectiveIconColor, ); final IconButtonThemeData iconButtonThemeData = IconButtonThemeData( style: IconButtonTheme.of(context).style?.copyWith( foregroundColor: WidgetStatePropertyAll( effectiveIconButtonColor, ), ) ?? IconButton.styleFrom(foregroundColor: effectiveIconButtonColor), ); TextStyle? leadingAndTrailingStyle; if (leading != null || trailing != null) { leadingAndTrailingStyle = leadingAndTrailingTextStyle ?? tileTheme.leadingAndTrailingTextStyle ?? defaults.leadingAndTrailingTextStyle!; final Color? leadingAndTrailingTextColor = effectiveColor; leadingAndTrailingStyle = leadingAndTrailingStyle.copyWith( color: leadingAndTrailingTextColor, ); } Widget? leadingIcon; if (leading != null) { leadingIcon = AnimatedDefaultTextStyle( style: leadingAndTrailingStyle!, duration: kThemeChangeDuration, child: leading!, ); } TextStyle titleStyle = titleTextStyle ?? tileTheme.titleTextStyle ?? defaults.titleTextStyle!; final Color? titleColor = effectiveColor; titleStyle = titleStyle.copyWith( color: titleColor, fontSize: _isDenseLayout(theme, tileTheme) ? 13.0 : null, ); final Widget titleText = AnimatedDefaultTextStyle( style: titleStyle, duration: kThemeChangeDuration, child: title ?? const SizedBox(), ); Widget? subtitleText; TextStyle? subtitleStyle; if (subtitle != null) { subtitleStyle = subtitleTextStyle ?? tileTheme.subtitleTextStyle ?? defaults.subtitleTextStyle!; final Color? subtitleColor = effectiveColor; subtitleStyle = subtitleStyle.copyWith( color: subtitleColor, fontSize: _isDenseLayout(theme, tileTheme) ? 12.0 : null, ); subtitleText = AnimatedDefaultTextStyle( style: subtitleStyle, duration: kThemeChangeDuration, child: subtitle!, ); } Widget? trailingIcon; if (trailing != null) { trailingIcon = AnimatedDefaultTextStyle( style: leadingAndTrailingStyle!, duration: kThemeChangeDuration, child: trailing!, ); } final TextDirection textDirection = Directionality.of(context); final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection) ?? tileTheme.contentPadding?.resolve(textDirection) ?? defaults.contentPadding!.resolve(textDirection); // Show basic cursor when ListTile isn't enabled or gesture callbacks are null. final Set mouseStates = { if (!enabled || (onTap == null && onTapUp == null && onLongPress == null && onSecondaryTap == null && onSecondaryTapUp == null)) WidgetState.disabled, }; final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs(mouseCursor, mouseStates) ?? tileTheme.mouseCursor?.resolve(mouseStates) ?? WidgetStateMouseCursor.clickable.resolve(mouseStates); final ListTileTitleAlignment effectiveTitleAlignment = titleAlignment ?? tileTheme.titleAlignment ?? (theme.useMaterial3 ? ListTileTitleAlignment.threeLine : ListTileTitleAlignment.titleHeight); Widget child = IconTheme.merge( data: iconThemeData, child: IconButtonTheme( data: iconButtonThemeData, child: _ListTile( leading: leadingIcon, title: titleText, subtitle: subtitleText, trailing: trailingIcon, isDense: _isDenseLayout(theme, tileTheme), visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity, isThreeLine: isThreeLine ?? tileTheme.isThreeLine ?? theme.listTileTheme.isThreeLine ?? false, textDirection: textDirection, titleBaselineType: titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!, subtitleBaselineType: subtitleStyle?.textBaseline ?? defaults.subtitleTextStyle!.textBaseline!, horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16, minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? defaults.minVerticalPadding!, minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? defaults.minLeadingWidth!, minTileHeight: minTileHeight ?? tileTheme.minTileHeight, titleAlignment: effectiveTitleAlignment, ), ), ); if (safeArea) { child = SafeArea( top: false, bottom: false, minimum: resolvedContentPadding, child: child, ); } else { child = Padding( padding: resolvedContentPadding, child: child, ); } return InkWell( customBorder: shape ?? tileTheme.shape, onTap: enabled ? onTap : null, onTapUp: enabled ? onTapUp : null, onLongPress: enabled ? onLongPress : null, onSecondaryTap: enabled ? onSecondaryTap : null, onSecondaryTapUp: enabled ? onSecondaryTapUp : null, onFocusChange: onFocusChange, mouseCursor: effectiveMouseCursor, canRequestFocus: enabled, focusNode: focusNode, focusColor: focusColor, hoverColor: hoverColor, splashColor: splashColor, autofocus: autofocus, enableFeedback: enableFeedback ?? tileTheme.enableFeedback ?? true, statesController: statesController, child: Semantics( button: internalAddSemanticForOnTap && (onTap != null || onLongPress != null), selected: selected, enabled: enabled, child: Ink( decoration: ShapeDecoration( shape: shape ?? tileTheme.shape ?? const Border(), color: _tileBackgroundColor(theme, tileTheme, defaults), ), child: child, ), ), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( FlagProperty( 'isThreeLine', value: isThreeLine, ifTrue: 'THREE_LINE', ifFalse: 'TWO_LINE', showName: true, ), ) ..add( FlagProperty( 'dense', value: dense, ifTrue: 'true', ifFalse: 'false', showName: true, ), ) ..add( DiagnosticsProperty( 'visualDensity', visualDensity, defaultValue: null, ), ) ..add( DiagnosticsProperty('shape', shape, defaultValue: null), ) ..add( DiagnosticsProperty('style', style, defaultValue: null), ) ..add( ColorProperty('selectedColor', selectedColor, defaultValue: null), ) ..add(ColorProperty('iconColor', iconColor, defaultValue: null)) ..add(ColorProperty('textColor', textColor, defaultValue: null)) ..add( DiagnosticsProperty( 'titleTextStyle', titleTextStyle, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'subtitleTextStyle', subtitleTextStyle, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'leadingAndTrailingTextStyle', leadingAndTrailingTextStyle, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'contentPadding', contentPadding, defaultValue: null, ), ) ..add( FlagProperty( 'enabled', value: enabled, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: true, ), ) ..add( DiagnosticsProperty('onTap', onTap, defaultValue: null), ) ..add( DiagnosticsProperty( 'onLongPress', onLongPress, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'mouseCursor', mouseCursor, defaultValue: null, ), ) ..add( FlagProperty( 'selected', value: selected, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: false, ), ) ..add(ColorProperty('focusColor', focusColor, defaultValue: null)) ..add(ColorProperty('hoverColor', hoverColor, defaultValue: null)) ..add( DiagnosticsProperty( 'focusNode', focusNode, defaultValue: null, ), ) ..add( FlagProperty( 'autofocus', value: autofocus, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: false, ), ) ..add(ColorProperty('tileColor', tileColor, defaultValue: null)) ..add( ColorProperty( 'selectedTileColor', selectedTileColor, defaultValue: null, ), ) ..add( FlagProperty( 'enableFeedback', value: enableFeedback, ifTrue: 'true', ifFalse: 'false', showName: true, ), ) ..add( DoubleProperty( 'horizontalTitleGap', horizontalTitleGap, defaultValue: null, ), ) ..add( DoubleProperty( 'minVerticalPadding', minVerticalPadding, defaultValue: null, ), ) ..add( DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null), ) ..add( DiagnosticsProperty( 'titleAlignment', titleAlignment, defaultValue: null, ), ); } } class _IndividualOverrides extends WidgetStateProperty { _IndividualOverrides({ this.explicitColor, this.enabledColor, this.selectedColor, this.disabledColor, }); final Color? explicitColor; final Color? enabledColor; final Color? selectedColor; final Color? disabledColor; @override Color? resolve(Set states) { if (explicitColor is WidgetStateColor) { return WidgetStateProperty.resolveAs(explicitColor, states); } if (states.contains(WidgetState.disabled)) { return disabledColor; } if (states.contains(WidgetState.selected)) { return selectedColor; } return enabledColor; } } // Identifies the children of a _ListTileElement. enum _ListTileSlot { leading, title, subtitle, trailing } class _ListTile extends SlottedMultiChildRenderObjectWidget<_ListTileSlot, RenderBox> { const _ListTile({ this.leading, required this.title, this.subtitle, this.trailing, required this.isThreeLine, required this.isDense, required this.visualDensity, required this.textDirection, required this.titleBaselineType, required this.horizontalTitleGap, required this.minVerticalPadding, required this.minLeadingWidth, this.minTileHeight, this.subtitleBaselineType, required this.titleAlignment, }); final Widget? leading; final Widget title; final Widget? subtitle; final Widget? trailing; final bool isThreeLine; final bool isDense; final VisualDensity visualDensity; final TextDirection textDirection; final TextBaseline titleBaselineType; final TextBaseline? subtitleBaselineType; final double horizontalTitleGap; final double minVerticalPadding; final double minLeadingWidth; final double? minTileHeight; final ListTileTitleAlignment titleAlignment; @override Iterable<_ListTileSlot> get slots => _ListTileSlot.values; @override Widget? childForSlot(_ListTileSlot slot) { return switch (slot) { _ListTileSlot.leading => leading, _ListTileSlot.title => title, _ListTileSlot.subtitle => subtitle, _ListTileSlot.trailing => trailing, }; } @override _RenderListTile createRenderObject(BuildContext context) { return _RenderListTile( isThreeLine: isThreeLine, isDense: isDense, visualDensity: visualDensity, textDirection: textDirection, titleBaselineType: titleBaselineType, subtitleBaselineType: subtitleBaselineType, horizontalTitleGap: horizontalTitleGap, minVerticalPadding: minVerticalPadding, minLeadingWidth: minLeadingWidth, minTileHeight: minTileHeight, titleAlignment: titleAlignment, ); } @override void updateRenderObject(BuildContext context, _RenderListTile renderObject) { renderObject ..isThreeLine = isThreeLine ..isDense = isDense ..visualDensity = visualDensity ..textDirection = textDirection ..titleBaselineType = titleBaselineType ..subtitleBaselineType = subtitleBaselineType ..horizontalTitleGap = horizontalTitleGap ..minLeadingWidth = minLeadingWidth ..minTileHeight = minTileHeight ..minVerticalPadding = minVerticalPadding ..titleAlignment = titleAlignment; } } class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ListTileSlot, RenderBox> { _RenderListTile({ required bool isDense, required VisualDensity visualDensity, required bool isThreeLine, required TextDirection textDirection, required TextBaseline titleBaselineType, TextBaseline? subtitleBaselineType, required double horizontalTitleGap, required double minVerticalPadding, required double minLeadingWidth, double? minTileHeight, required ListTileTitleAlignment titleAlignment, }) : _isDense = isDense, _visualDensity = visualDensity, _isThreeLine = isThreeLine, _textDirection = textDirection, _titleBaselineType = titleBaselineType, _subtitleBaselineType = subtitleBaselineType, _horizontalTitleGap = horizontalTitleGap, _minVerticalPadding = minVerticalPadding, _minLeadingWidth = minLeadingWidth, _minTileHeight = minTileHeight, _titleAlignment = titleAlignment; RenderBox? get leading => childForSlot(_ListTileSlot.leading); RenderBox get title => childForSlot(_ListTileSlot.title)!; RenderBox? get subtitle => childForSlot(_ListTileSlot.subtitle); RenderBox? get trailing => childForSlot(_ListTileSlot.trailing); // The returned list is ordered for hit testing. @override Iterable get children { final RenderBox? title = childForSlot(_ListTileSlot.title); return [?leading, ?title, ?subtitle, ?trailing]; } bool get isDense => _isDense; bool _isDense; set isDense(bool value) { if (_isDense == value) { return; } _isDense = value; markNeedsLayout(); } VisualDensity get visualDensity => _visualDensity; VisualDensity _visualDensity; set visualDensity(VisualDensity value) { if (_visualDensity == value) { return; } _visualDensity = value; markNeedsLayout(); } bool get isThreeLine => _isThreeLine; bool _isThreeLine; set isThreeLine(bool value) { if (_isThreeLine == value) { return; } _isThreeLine = value; markNeedsLayout(); } TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (_textDirection == value) { return; } _textDirection = value; markNeedsLayout(); } TextBaseline get titleBaselineType => _titleBaselineType; TextBaseline _titleBaselineType; set titleBaselineType(TextBaseline value) { if (_titleBaselineType == value) { return; } _titleBaselineType = value; markNeedsLayout(); } TextBaseline? get subtitleBaselineType => _subtitleBaselineType; TextBaseline? _subtitleBaselineType; set subtitleBaselineType(TextBaseline? value) { if (_subtitleBaselineType == value) { return; } _subtitleBaselineType = value; markNeedsLayout(); } double get horizontalTitleGap => _horizontalTitleGap; double _horizontalTitleGap; double get _effectiveHorizontalTitleGap => _horizontalTitleGap + visualDensity.horizontal * 2.0; set horizontalTitleGap(double value) { if (_horizontalTitleGap == value) { return; } _horizontalTitleGap = value; markNeedsLayout(); } double get minVerticalPadding => _minVerticalPadding; double _minVerticalPadding; set minVerticalPadding(double value) { if (_minVerticalPadding == value) { return; } _minVerticalPadding = value; markNeedsLayout(); } double get minLeadingWidth => _minLeadingWidth; double _minLeadingWidth; set minLeadingWidth(double value) { if (_minLeadingWidth == value) { return; } _minLeadingWidth = value; markNeedsLayout(); } double? _minTileHeight; double? get minTileHeight => _minTileHeight; set minTileHeight(double? value) { if (_minTileHeight == value) { return; } _minTileHeight = value; markNeedsLayout(); } ListTileTitleAlignment get titleAlignment => _titleAlignment; ListTileTitleAlignment _titleAlignment; set titleAlignment(ListTileTitleAlignment value) { if (_titleAlignment == value) { return; } _titleAlignment = value; markNeedsLayout(); } @override bool get sizedByParent => false; static double _minWidth(RenderBox? box, double height) { return box == null ? 0.0 : box.getMinIntrinsicWidth(height); } static double _maxWidth(RenderBox? box, double height) { return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); } @override double computeMinIntrinsicWidth(double height) { final double leadingWidth = leading != null ? math.max(leading!.getMinIntrinsicWidth(height), _minLeadingWidth) + _effectiveHorizontalTitleGap : 0.0; return leadingWidth + math.max(_minWidth(title, height), _minWidth(subtitle, height)) + _maxWidth(trailing, height); } @override double computeMaxIntrinsicWidth(double height) { final double leadingWidth = leading != null ? math.max(leading!.getMaxIntrinsicWidth(height), _minLeadingWidth) + _effectiveHorizontalTitleGap : 0.0; return leadingWidth + math.max(_maxWidth(title, height), _maxWidth(subtitle, height)) + _maxWidth(trailing, height); } // The target tile height to use if _minTileHeight is not specified. double get _defaultTileHeight { final Offset baseDensity = visualDensity.baseSizeAdjustment; return baseDensity.dy + switch ((isThreeLine, subtitle != null)) { (true, _) => isDense ? 76.0 : 88.0, // 3 lines, (false, true) => isDense ? 64.0 : 72.0, // 2 lines (false, false) => isDense ? 48.0 : 56.0, // 1 line, }; } double get _targetTileHeight => _minTileHeight ?? _defaultTileHeight; @override double computeMinIntrinsicHeight(double width) { final double titleMinHeight = title.getMinIntrinsicHeight(width); final double? subtitleMinHeight = subtitle?.getMinIntrinsicHeight(width); const topAndBottomPaddingMultiplier = 2; final double contentHeight = titleMinHeight + (subtitleMinHeight ?? 0.0) + topAndBottomPaddingMultiplier * _minVerticalPadding; return math.max(_targetTileHeight, contentHeight); } @override double computeMaxIntrinsicHeight(double width) { return getMinIntrinsicHeight(width); } @override double? computeDistanceToActualBaseline(TextBaseline baseline) { final BoxParentData parentData = title.parentData! as BoxParentData; final BaselineOffset offset = BaselineOffset(title.getDistanceToActualBaseline(baseline)) + parentData.offset.dy; return offset.offset; } BoxConstraints get maxIconHeightConstraint => BoxConstraints( // One-line trailing and leading widget heights do not follow // Material specifications, but this sizing is required to adhere // to accessibility requirements for smallest tappable widget. // Two- and three-line trailing widget heights are constrained // properly according to the Material spec. maxHeight: (isDense ? 48.0 : 56.0) + visualDensity.baseSizeAdjustment.dy, ); static void _positionBox(RenderBox box, Offset offset) { final BoxParentData parentData = box.parentData! as BoxParentData; parentData.offset = offset; } // Implements _RenderListTile's layout algorithm. If `positionChild` is not null, // it will be called on each child with that child's layout offset. // // All of the dimensions below were taken from the Material Design spec: // https://material.io/design/components/lists.html#specs _Sizes _computeSizes( ChildBaselineGetter getBaseline, ChildLayouter getSize, BoxConstraints constraints, { _PositionChild? positionChild, }) { final BoxConstraints looseConstraints = constraints.loosen(); final double tileWidth = looseConstraints.maxWidth; final BoxConstraints iconConstraints = looseConstraints.enforce( maxIconHeightConstraint, ); final RenderBox? leading = this.leading; final RenderBox? trailing = this.trailing; final Size? leadingSize = leading == null ? null : getSize(leading, iconConstraints); final Size? trailingSize = trailing == null ? null : getSize(trailing, iconConstraints); assert(() { if (tileWidth == 0.0) { return true; } String? overflowedWidget; if (tileWidth == leadingSize?.width) { overflowedWidget = 'Leading'; } else if (tileWidth == trailingSize?.width) { overflowedWidget = 'Trailing'; } if (overflowedWidget == null) { return true; } throw FlutterError.fromParts([ ErrorSummary( '$overflowedWidget widget consumes the entire tile width (including ListTile.contentPadding).', ), ErrorDescription( 'Either resize the tile width so that the ${overflowedWidget.toLowerCase()} widget plus any content padding ' 'do not exceed the tile width, or use a sized widget, or consider replacing ' 'ListTile with a custom widget.', ), ErrorHint( 'See also: https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4', ), ]); }()); final double titleStart = leadingSize == null ? 0.0 : math.max(_minLeadingWidth, leadingSize.width) + _effectiveHorizontalTitleGap; final double adjustedTrailingWidth = trailingSize == null ? 0.0 : math.max(trailingSize.width + _effectiveHorizontalTitleGap, 32.0); final BoxConstraints textConstraints = looseConstraints.tighten( width: tileWidth - titleStart - adjustedTrailingWidth, ); final RenderBox? subtitle = this.subtitle; final double titleHeight = getSize(title, textConstraints).height; final bool isLTR = switch (textDirection) { TextDirection.ltr => true, TextDirection.rtl => false, }; final double titleY; final double tileHeight; if (subtitle == null) { tileHeight = math.max( _targetTileHeight, titleHeight + 2.0 * _minVerticalPadding, ); titleY = (tileHeight - titleHeight) / 2.0; } else { final double subtitleHeight = getSize(subtitle, textConstraints).height; final double titleBaseline = getBaseline(title, textConstraints, titleBaselineType) ?? titleHeight; final double subtitleBaseline = getBaseline(subtitle, textConstraints, subtitleBaselineType!) ?? subtitleHeight; final double targetTitleY = (isThreeLine ? (isDense ? 22.0 : 28.0) : (isDense ? 28.0 : 32.0)) - titleBaseline; final double targetSubtitleY = (isThreeLine ? (isDense ? 42.0 : 48.0) : (isDense ? 48.0 : 52.0)) + visualDensity.vertical * 2.0 - subtitleBaseline; // Prevent the title and the subtitle from overlapping by moving them away from // each other by the same distance. final double halfOverlap = math.max(targetTitleY + titleHeight - targetSubtitleY, 0) / 2; final double idealTitleY = targetTitleY - halfOverlap; final double idealSubtitleY = targetSubtitleY + halfOverlap; // However if either component can't maintain the minimal padding from the top/bottom edges, the ListTile enters "compat mode". final bool compact = idealTitleY < minVerticalPadding || idealSubtitleY + subtitleHeight + minVerticalPadding > _targetTileHeight; // Position subtitle. positionChild?.call( subtitle, Offset( isLTR ? titleStart : adjustedTrailingWidth, compact ? minVerticalPadding + titleHeight : idealSubtitleY, ), ); tileHeight = compact ? 2 * _minVerticalPadding + titleHeight + subtitleHeight : _targetTileHeight; titleY = compact ? minVerticalPadding : idealTitleY; } if (positionChild != null) { positionChild( title, Offset(isLTR ? titleStart : adjustedTrailingWidth, titleY), ); if (leading != null && leadingSize != null) { positionChild( leading, Offset( isLTR ? 0.0 : tileWidth - leadingSize.width, titleAlignment._yOffsetFor( leadingSize.height, tileHeight, this, true, ), ), ); } if (trailing != null && trailingSize != null) { positionChild( trailing, Offset( isLTR ? tileWidth - trailingSize.width : 0.0, titleAlignment._yOffsetFor( trailingSize.height, tileHeight, this, false, ), ), ); } } return ( titleY: titleY, textConstraints: textConstraints, tileSize: Size(tileWidth, tileHeight), ); } @override double? computeDryBaseline( covariant BoxConstraints constraints, TextBaseline baseline, ) { final _Sizes sizes = _computeSizes( ChildLayoutHelper.getDryBaseline, ChildLayoutHelper.dryLayoutChild, constraints, ); final BaselineOffset titleBaseline = BaselineOffset(title.getDryBaseline(sizes.textConstraints, baseline)) + sizes.titleY; return titleBaseline.offset; } @override Size computeDryLayout(BoxConstraints constraints) { return constraints.constrain( _computeSizes( ChildLayoutHelper.getDryBaseline, ChildLayoutHelper.dryLayoutChild, constraints, ).tileSize, ); } @override void performLayout() { final Size tileSize = _computeSizes( ChildLayoutHelper.getBaseline, ChildLayoutHelper.layoutChild, constraints, positionChild: _positionBox, ).tileSize; size = constraints.constrain(tileSize); assert(size.width == constraints.constrainWidth(tileSize.width)); assert(size.height == constraints.constrainHeight(tileSize.height)); } @override void paint(PaintingContext context, Offset offset) { void doPaint(RenderBox? child) { if (child != null) { final BoxParentData parentData = child.parentData! as BoxParentData; context.paintChild(child, parentData.offset + offset); } } doPaint(leading); doPaint(title); doPaint(subtitle); doPaint(trailing); } @override bool hitTestSelf(Offset position) => true; @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { for (final RenderBox child in children) { final BoxParentData parentData = child.parentData! as BoxParentData; final bool isHit = result.addWithPaintOffset( offset: parentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset transformed) { assert(transformed == position - parentData.offset); return child.hitTest(result, position: transformed); }, ); if (isHit) { return true; } } return false; } } class _LisTileDefaultsM2 extends ListTileThemeData { _LisTileDefaultsM2(this.context, ListTileStyle style) : super( contentPadding: const EdgeInsets.symmetric(horizontal: 16.0), minLeadingWidth: 40, minVerticalPadding: 4, shape: const Border(), style: style, ); final BuildContext context; late final ThemeData _theme = Theme.of(context); late final TextTheme _textTheme = _theme.textTheme; @override Color? get tileColor => Colors.transparent; @override TextStyle? get titleTextStyle => switch (style!) { ListTileStyle.drawer => _textTheme.bodyLarge, ListTileStyle.list => _textTheme.titleMedium, }; @override TextStyle? get subtitleTextStyle => _textTheme.bodyMedium!.copyWith(color: _textTheme.bodySmall!.color); @override TextStyle? get leadingAndTrailingTextStyle => _textTheme.bodyMedium; @override Color? get selectedColor => _theme.colorScheme.primary; @override Color? get iconColor => switch (_theme.brightness) { // For the sake of backwards compatibility, the default for unselected // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73). Brightness.light => Colors.black45, // null -> use current icon theme color Brightness.dark => null, }; } // BEGIN GENERATED TOKEN PROPERTIES - LisTile // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // dart format off class _LisTileDefaultsM3 extends ListTileThemeData { _LisTileDefaultsM3(this.context) : super( contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 24.0), minLeadingWidth: 24, minVerticalPadding: 8, shape: const RoundedRectangleBorder(), ); final BuildContext context; late final ThemeData _theme = Theme.of(context); late final ColorScheme _colors = _theme.colorScheme; late final TextTheme _textTheme = _theme.textTheme; @override Color? get tileColor => Colors.transparent; @override TextStyle? get titleTextStyle => _textTheme.bodyLarge!.copyWith(color: _colors.onSurface); @override TextStyle? get subtitleTextStyle => _textTheme.bodyMedium!.copyWith(color: _colors.onSurfaceVariant); @override TextStyle? get leadingAndTrailingTextStyle => _textTheme.labelSmall!.copyWith(color: _colors.onSurfaceVariant); @override Color? get selectedColor => _colors.primary; @override Color? get iconColor => _colors.onSurfaceVariant; } // dart format on // END GENERATED TOKEN PROPERTIES - LisTile ================================================ FILE: lib/common/widgets/flutter/page/page_view.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; import 'package:flutter/material.dart' hide PageView, Scrollable, ScrollableState; import 'package:flutter/rendering.dart'; class _ForceImplicitScrollPhysics extends ScrollPhysics { const _ForceImplicitScrollPhysics({ required this.allowImplicitScrolling, super.parent, }); @override _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { return _ForceImplicitScrollPhysics( allowImplicitScrolling: allowImplicitScrolling, parent: buildParent(ancestor), ); } @override final bool allowImplicitScrolling; } const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); /// A scrollable list that works page by page. /// /// Each child of a page view is forced to be the same size as the viewport. /// /// You can use a [PageController] to control which page is visible in the view. /// In addition to being able to control the pixel offset of the content inside /// the [PageView], a [PageController] also lets you control the offset in terms /// of pages, which are increments of the viewport size. /// /// The [PageController] can also be used to control the /// [PageController.initialPage], which determines which page is shown when the /// [PageView] is first constructed, and the [PageController.viewportFraction], /// which determines the size of the pages as a fraction of the viewport size. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A} /// /// {@tool dartpad} /// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages /// which scroll horizontally. /// /// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart ** /// {@end-tool} /// /// ## Persisting the scroll position during a session /// /// Scroll views attempt to persist their scroll position using [PageStorage]. /// For a [PageView], this can be disabled by setting [PageController.keepPage] /// to false on the [controller]. If it is enabled, using a [PageStorageKey] for /// the [key] of this widget is recommended to help disambiguate different /// scroll views from each other. /// /// See also: /// /// * [PageController], which controls which page is visible in the view. /// * [SingleChildScrollView], when you need to make a single child scrollable. /// * [ListView], for a scrollable list of boxes. /// * [GridView], for a scrollable grid of boxes. /// * [ScrollNotification] and [NotificationListener], which can be used to watch /// the scroll position without using a [ScrollController]. class PageView extends StatefulWidget { /// Creates a scrollable list that works page by page from an explicit [List] /// of widgets. /// /// This constructor is appropriate for page views with a small number of /// children because constructing the [List] requires doing work for every /// child that could possibly be displayed in the page view, instead of just /// those children that are actually visible. /// /// Like other widgets in the framework, this widget expects that /// the [children] list will not be mutated after it has been passed in here. /// See the documentation at [SliverChildListDelegate.children] for more details. /// /// {@template flutter.widgets.PageView.allowImplicitScrolling} /// If [allowImplicitScrolling] is true, the [PageView] will participate in /// accessibility scrolling more like a [ListView], where implicit scroll /// actions will move to the next page rather than into the contents of the /// [PageView]. /// {@endtemplate} PageView({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, this.controller, this.physics, this.pageSnapping = true, this.onPageChanged, List children = const [], this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, required this.horizontalDragGestureRecognizer, }) : childrenDelegate = SliverChildListDelegate(children); final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; /// Creates a scrollable list that works page by page using widgets that are /// created on demand. /// /// This constructor is appropriate for page views with a large (or infinite) /// number of children because the builder is called only for those children /// that are actually visible. /// /// Providing a non-null [itemCount] lets the [PageView] compute the maximum /// scroll extent. /// /// [itemBuilder] will be called only with indices greater than or equal to /// zero and less than [itemCount]. /// /// {@macro flutter.widgets.ListView.builder.itemBuilder} /// /// {@template flutter.widgets.PageView.findChildIndexCallback} /// The [findChildIndexCallback] corresponds to the /// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null, /// a child widget may not map to its existing [RenderObject] when the order /// of children returned from the children builder changes. /// This may result in state-loss. This callback needs to be implemented if /// the order of the children may change at a later time. /// {@endtemplate} /// /// {@macro flutter.widgets.PageView.allowImplicitScrolling} PageView.builder({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, this.controller, this.physics, this.pageSnapping = true, this.onPageChanged, required NullableIndexedWidgetBuilder itemBuilder, ChildIndexGetter? findChildIndexCallback, int? itemCount, this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, required this.horizontalDragGestureRecognizer, }) : childrenDelegate = SliverChildBuilderDelegate( itemBuilder, findChildIndexCallback: findChildIndexCallback, childCount: itemCount, ); /// Creates a scrollable list that works page by page with a custom child /// model. /// /// {@tool dartpad} /// This example shows a [PageView] that uses a custom [SliverChildBuilderDelegate] to support child /// reordering. /// /// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart ** /// {@end-tool} /// /// {@macro flutter.widgets.PageView.allowImplicitScrolling} const PageView.custom({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, this.controller, this.physics, this.pageSnapping = true, this.onPageChanged, required this.childrenDelegate, this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, required this.horizontalDragGestureRecognizer, }); /// Controls whether the widget's pages will respond to /// [RenderObject.showOnScreen], which will allow for implicit accessibility /// scrolling. /// /// With this flag set to false, when accessibility focus reaches the end of /// the current page and the user attempts to move it to the next element, the /// focus will traverse to the next widget outside of the page view. /// /// With this flag set to true, when accessibility focus reaches the end of /// the current page and user attempts to move it to the next element, focus /// will traverse to the next page in the page view. final bool allowImplicitScrolling; /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; /// The [Axis] along which the scroll view's offset increases with each page. /// /// For the direction in which active scrolling may be occurring, see /// [ScrollDirection]. /// /// Defaults to [Axis.horizontal]. final Axis scrollDirection; /// Whether the page view scrolls in the reading direction. /// /// For example, if the reading direction is left-to-right and /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from /// left to right when [reverse] is false and from right to left when /// [reverse] is true. /// /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view /// scrolls from top to bottom when [reverse] is false and from bottom to top /// when [reverse] is true. /// /// Defaults to false. final bool reverse; /// An object that can be used to control the position to which this page /// view is scrolled. final PageController? controller; /// How the page view should respond to user input. /// /// For example, determines how the page view continues to animate after the /// user stops dragging the page view. /// /// The physics are modified to snap to page boundaries using /// [PageScrollPhysics] prior to being used. /// /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the /// [ScrollPhysics] provided by that behavior will take precedence after /// [physics]. /// /// Defaults to matching platform conventions. final ScrollPhysics? physics; /// Set to false to disable page snapping, useful for custom scroll behavior. /// /// If the [padEnds] is false and [PageController.viewportFraction] < 1.0, /// the page will snap to the beginning of the viewport; otherwise, the page /// will snap to the center of the viewport. final bool pageSnapping; /// Called whenever the page in the center of the viewport changes. final ValueChanged? onPageChanged; /// A delegate that provides the children for the [PageView]. /// /// The [PageView.custom] constructor lets you specify this delegate /// explicitly. The [PageView] and [PageView.builder] constructors create a /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], /// respectively. final SliverChildDelegate childrenDelegate; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// {@macro flutter.widgets.scrollable.hitTestBehavior} /// /// Defaults to [HitTestBehavior.opaque]. final HitTestBehavior hitTestBehavior; /// {@macro flutter.widgets.scrollable.scrollBehavior} /// /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be /// modified by default to not apply a [Scrollbar]. final ScrollBehavior? scrollBehavior; /// Whether to add padding to both ends of the list. /// /// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added /// such that the first and last child slivers will be in the center of /// the viewport when scrolled all the way to the start or end, respectively. /// /// If [PageController.viewportFraction] >= 1.0, this property has no effect. /// /// This property defaults to true. final bool padEnds; @override State> createState() => _PageViewState(); } class _PageViewState extends State> { int _lastReportedPage = 0; late PageController _controller; @override void initState() { super.initState(); _initController(); _lastReportedPage = _controller.initialPage; } @override void dispose() { if (widget.controller == null) { _controller.dispose(); } super.dispose(); } void _initController() { _controller = widget.controller ?? PageController(); } @override void didUpdateWidget(PageView oldWidget) { if (oldWidget.controller != widget.controller) { if (oldWidget.controller == null) { _controller.dispose(); } _initController(); } super.didUpdateWidget(oldWidget); } AxisDirection _getDirection(BuildContext context) { switch (widget.scrollDirection) { case Axis.horizontal: assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); final AxisDirection axisDirection = textDirectionToAxisDirection( textDirection, ); return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection; case Axis.vertical: return widget.reverse ? AxisDirection.up : AxisDirection.down; } } @override Widget build(BuildContext context) { final AxisDirection axisDirection = _getDirection(context); final ScrollPhysics physics = _ForceImplicitScrollPhysics( allowImplicitScrolling: widget.allowImplicitScrolling, ).applyTo( widget.pageSnapping ? _kPagePhysics.applyTo( widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context), ) : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context), ); return NotificationListener( onNotification: (ScrollNotification notification) { if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) { final metrics = notification.metrics as PageMetrics; final int currentPage = metrics.page!.round(); if (currentPage != _lastReportedPage) { _lastReportedPage = currentPage; widget.onPageChanged!(currentPage); } } return false; }, child: Scrollable( dragStartBehavior: widget.dragStartBehavior, axisDirection: axisDirection, controller: _controller, physics: physics, restorationId: widget.restorationId, hitTestBehavior: widget.hitTestBehavior, scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false), viewportBuilder: (BuildContext context, ViewportOffset position) { return Viewport( // TODO(dnfield): we should provide a way to set cacheExtent // independent of implicit scrolling: // https://github.com/flutter/flutter/issues/45632 cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0, cacheExtentStyle: CacheExtentStyle.viewport, axisDirection: axisDirection, offset: position, clipBehavior: widget.clipBehavior, slivers: [ SliverFillViewport( viewportFraction: _controller.viewportFraction, delegate: widget.childrenDelegate, padEnds: widget.padEnds, ), ], ); }, horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, ), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description ..add(EnumProperty('scrollDirection', widget.scrollDirection)) ..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')) ..add( DiagnosticsProperty( 'controller', _controller, showName: false, ), ) ..add( DiagnosticsProperty( 'physics', widget.physics, showName: false, ), ) ..add( FlagProperty( 'pageSnapping', value: widget.pageSnapping, ifFalse: 'snapping disabled', ), ) ..add( FlagProperty( 'allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling', ), ); } } ================================================ FILE: lib/common/widgets/flutter/page/scrollable.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/page/scrollable_helpers.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Scrollable, ScrollableState, EdgeDraggingAutoScroller; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; // The return type of _performEnsureVisible. // // The list of futures represents each pending ScrollPosition call to // ensureVisible. The returned ScrollableState's context is used to find the // next potential ancestor Scrollable. typedef _EnsureVisibleResults = (List>, ScrollableState); /// A widget that manages scrolling in one dimension and informs the [Viewport] /// through which the content is viewed. /// /// [Scrollable] implements the interaction model for a scrollable widget, /// including gesture recognition, but does not have an opinion about how the /// viewport, which actually displays the children, is constructed. /// /// It's rare to construct a [Scrollable] directly. Instead, consider [ListView] /// or [GridView], which combine scrolling, viewporting, and a layout model. To /// combine layout models (or to use a custom layout mode), consider using /// [CustomScrollView]. /// /// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are /// often used to interact with the [Scrollable] widget inside a [ListView] or /// a [GridView]. /// /// To further customize scrolling behavior with a [Scrollable]: /// /// 1. You can provide a [viewportBuilder] to customize the child model. For /// example, [SingleChildScrollView] uses a viewport that displays a single /// box child whereas [CustomScrollView] uses a [Viewport] or a /// [ShrinkWrappingViewport], both of which display a list of slivers. /// /// 2. You can provide a custom [ScrollController] that creates a custom /// [ScrollPosition] subclass. For example, [PageView] uses a /// [PageController], which creates a page-oriented scroll position subclass /// that keeps the same page visible when the [Scrollable] resizes. /// /// ## Persisting the scroll position during a session /// /// Scrollables attempt to persist their scroll position using [PageStorage]. /// This can be disabled by setting [ScrollController.keepScrollOffset] to false /// on the [controller]. If it is enabled, using a [PageStorageKey] for the /// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is /// recommended to help disambiguate different [Scrollable]s from each other. /// /// See also: /// /// * [ListView], which is a commonly used [ScrollView] that displays a /// scrolling, linear list of child widgets. /// * [PageView], which is a scrolling list of child widgets that are each the /// size of the viewport. /// * [GridView], which is a [ScrollView] that displays a scrolling, 2D array /// of child widgets. /// * [CustomScrollView], which is a [ScrollView] that creates custom scroll /// effects using slivers. /// * [SingleChildScrollView], which is a scrollable widget that has a single /// child. /// * [ScrollNotification] and [NotificationListener], which can be used to watch /// the scroll position without using a [ScrollController]. class Scrollable extends StatefulWidget { /// Creates a widget that scrolls. const Scrollable({ super.key, this.axisDirection = AxisDirection.down, this.controller, this.physics, required this.viewportBuilder, this.incrementCalculator, this.excludeFromSemantics = false, this.semanticChildCount, this.dragStartBehavior = DragStartBehavior.start, this.restorationId, this.scrollBehavior, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, required this.horizontalDragGestureRecognizer, }) : assert(semanticChildCount == null || semanticChildCount >= 0); final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; /// {@template flutter.widgets.Scrollable.axisDirection} /// The direction in which this widget scrolls. /// /// For example, if the [Scrollable.axisDirection] is [AxisDirection.down], /// increasing the scroll position will cause content below the bottom of the /// viewport to become visible through the viewport. Similarly, if the /// axisDirection is [AxisDirection.right], increasing the scroll position /// will cause content beyond the right edge of the viewport to become visible /// through the viewport. /// /// Defaults to [AxisDirection.down]. /// {@endtemplate} final AxisDirection axisDirection; /// {@template flutter.widgets.Scrollable.controller} /// An object that can be used to control the position to which this widget is /// scrolled. /// /// A [ScrollController] serves several purposes. It can be used to control /// the initial scroll position (see [ScrollController.initialScrollOffset]). /// It can be used to control whether the scroll view should automatically /// save and restore its scroll position in the [PageStorage] (see /// [ScrollController.keepScrollOffset]). It can be used to read the current /// scroll position (see [ScrollController.offset]), or change it (see /// [ScrollController.animateTo]). /// /// If null, a [ScrollController] will be created internally by [Scrollable] /// in order to create and manage the [ScrollPosition]. /// /// See also: /// /// * [Scrollable.ensureVisible], which animates the scroll position to /// reveal a given [BuildContext]. /// {@endtemplate} final ScrollController? controller; /// {@template flutter.widgets.Scrollable.physics} /// How the widgets should respond to user input. /// /// For example, determines how the widget continues to animate after the /// user stops dragging the scroll view. /// /// Defaults to matching platform conventions via the physics provided from /// the ambient [ScrollConfiguration]. /// /// If an explicit [ScrollBehavior] is provided to /// [Scrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior /// will take precedence after [Scrollable.physics]. /// /// The physics can be changed dynamically, but new physics will only take /// effect if the _class_ of the provided object changes. Merely constructing /// a new instance with a different configuration is insufficient to cause the /// physics to be reapplied. (This is because the final object used is /// generated dynamically, which can be relatively expensive, and it would be /// inefficient to speculatively create this object each frame to see if the /// physics should be updated.) /// /// See also: /// /// * [AlwaysScrollableScrollPhysics], which can be used to indicate that the /// scrollable should react to scroll requests (and possible overscroll) /// even if the scrollable's contents fit without scrolling being necessary. /// {@endtemplate} final ScrollPhysics? physics; /// Builds the viewport through which the scrollable content is displayed. /// /// A typical viewport uses the given [ViewportOffset] to determine which part /// of its content is actually visible through the viewport. /// /// See also: /// /// * [Viewport], which is a viewport that displays a list of slivers. /// * [ShrinkWrappingViewport], which is a viewport that displays a list of /// slivers and sizes itself based on the size of the slivers. final ViewportBuilder viewportBuilder; /// {@template flutter.widgets.Scrollable.incrementCalculator} /// An optional function that will be called to calculate the distance to /// scroll when the scrollable is asked to scroll via the keyboard using a /// [ScrollAction]. /// /// If not supplied, the [Scrollable] will scroll a default amount when a /// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow, /// etc.), or otherwise invoked by a [ScrollAction]. /// /// If [incrementCalculator] is null, the default for /// [ScrollIncrementType.page] is 80% of the size of the scroll window, and /// for [ScrollIncrementType.line], 50 logical pixels. /// {@endtemplate} final ScrollIncrementCalculator? incrementCalculator; /// {@template flutter.widgets.scrollable.excludeFromSemantics} /// Whether the scroll actions introduced by this [Scrollable] are exposed /// in the semantics tree. /// /// Text fields with an overflow are usually scrollable to make sure that the /// user can get to the beginning/end of the entered text. However, these /// scrolling actions are generally not exposed to the semantics layer. /// {@endtemplate} /// /// See also: /// /// * [GestureDetector.excludeFromSemantics], which is used to accomplish the /// exclusion. final bool excludeFromSemantics; /// {@template flutter.widgets.scrollable.hitTestBehavior} /// Defines the behavior of gesture detector used in this [Scrollable]. /// /// This defaults to [HitTestBehavior.opaque] which means it prevents targets /// behind this [Scrollable] from receiving events. /// {@endtemplate} /// /// See also: /// /// * [HitTestBehavior], for an explanation on different behaviors. final HitTestBehavior hitTestBehavior; /// The number of children that will contribute semantic information. /// /// The value will be null if the number of children is unknown or unbounded. /// /// Some subtypes of [ScrollView] can infer this value automatically. For /// example [ListView] will use the number of widgets in the child list, /// while the [ListView.separated] constructor will use half that amount. /// /// For [CustomScrollView] and other types which do not receive a builder /// or list of widgets, the child count must be explicitly provided. /// /// See also: /// /// * [CustomScrollView], for an explanation of scroll semantics. /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. final int? semanticChildCount; // TODO(jslavitz): Set the DragStartBehavior default to be start across all widgets. /// {@template flutter.widgets.scrollable.dragStartBehavior} /// Determines the way that drag start behavior is handled. /// /// If set to [DragStartBehavior.start], scrolling drag behavior will /// begin at the position where the drag gesture won the arena. If set to /// [DragStartBehavior.down] it will begin at the position where a down /// event is first detected. /// /// In general, setting this to [DragStartBehavior.start] will make drag /// animation smoother and setting it to [DragStartBehavior.down] will make /// drag behavior feel slightly more reactive. /// /// By default, the drag start behavior is [DragStartBehavior.start]. /// /// See also: /// /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for /// the different behaviors. /// /// {@endtemplate} final DragStartBehavior dragStartBehavior; /// {@template flutter.widgets.scrollable.restorationId} /// Restoration ID to save and restore the scroll offset of the scrollable. /// /// If a restoration id is provided, the scrollable will persist its current /// scroll offset and restore it during state restoration. /// /// The scroll offset is persisted in a [RestorationBucket] claimed from /// the surrounding [RestorationScope] using the provided restoration ID. /// /// See also: /// /// * [RestorationManager], which explains how state restoration works in /// Flutter. /// {@endtemplate} final String? restorationId; /// {@template flutter.widgets.scrollable.scrollBehavior} /// A [ScrollBehavior] that will be applied to this widget individually. /// /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and /// modified to alter the viewport decoration, like [Scrollbar]s. /// /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit /// [ScrollPhysics] is provided in [physics], it will take precedence, /// followed by [scrollBehavior], and then the inherited ancestor /// [ScrollBehavior]. /// {@endtemplate} final ScrollBehavior? scrollBehavior; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. /// /// This is passed to decorators in [ScrollableDetails], and does not directly affect /// clipping of the [Scrollable]. This reflects the same [Clip] that is provided /// to [ScrollView.clipBehavior] and is supplied to the [Viewport]. final Clip clipBehavior; /// The axis along which the scroll view scrolls. /// /// Determined by the [axisDirection]. Axis get axis => axisDirectionToAxis(axisDirection); @override ScrollableState createState() => ScrollableState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(EnumProperty('axisDirection', axisDirection)) ..add(DiagnosticsProperty('physics', physics)) ..add(StringProperty('restorationId', restorationId)); } /// The state from the closest instance of this class that encloses the given /// context, or null if none is found. /// /// Typical usage is as follows: /// /// ```dart /// ScrollableState? scrollable = Scrollable.maybeOf(context); /// ``` /// /// Calling this method will create a dependency on the [ScrollableState] /// that is returned, if there is one. This is typically the closest /// [Scrollable], but may be a more distant ancestor if [axis] is used to /// target a specific [Scrollable]. /// /// Using the optional [Axis] is useful when Scrollables are nested and the /// target [Scrollable] is not the closest instance. When [axis] is provided, /// the nearest enclosing [ScrollableState] in that [Axis] is returned, or /// null if there is none. /// /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This /// means that if the `context` is that of a [Scrollable], it will _not_ find /// _that_ [Scrollable]. /// /// See also: /// /// * [Scrollable.of], which is similar to this method, but asserts /// if no [Scrollable] ancestor is found. static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) { // This is the context that will need to establish the dependency. final originalContext = context; InheritedElement? element = context .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); while (element != null) { final ScrollableState scrollable = (element.widget as _ScrollableScope).scrollable; if (axis == null || axisDirectionToAxis(scrollable.axisDirection) == axis) { // Establish the dependency on the correct context. originalContext.dependOnInheritedElement(element); return scrollable; } context = scrollable.context; element = context .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); } return null; } /// The state from the closest instance of this class that encloses the given /// context. /// /// Typical usage is as follows: /// /// ```dart /// ScrollableState scrollable = Scrollable.of(context); /// ``` /// /// Calling this method will create a dependency on the [ScrollableState] /// that is returned, if there is one. This is typically the closest /// [Scrollable], but may be a more distant ancestor if [axis] is used to /// target a specific [Scrollable]. /// /// Using the optional [Axis] is useful when Scrollables are nested and the /// target [Scrollable] is not the closest instance. When [axis] is provided, /// the nearest enclosing [ScrollableState] in that [Axis] is returned. /// /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This /// means that if the `context` is that of a [Scrollable], it will _not_ find /// _that_ [Scrollable]. /// /// If no [Scrollable] ancestor is found, then this method will assert in /// debug mode, and throw an exception in release mode. /// /// See also: /// /// * [Scrollable.maybeOf], which is similar to this method, but returns null /// if no [Scrollable] ancestor is found. static ScrollableState of(BuildContext context, {Axis? axis}) { final ScrollableState? scrollableState = maybeOf(context, axis: axis); assert(() { if (scrollableState == null) { throw FlutterError.fromParts([ ErrorSummary( 'Scrollable.of() was called with a context that does not contain a ' 'Scrollable widget.', ), ErrorDescription( 'No Scrollable widget ancestor could be found ' '${axis == null ? '' : 'for the provided Axis: $axis '}' 'starting from the context that was passed to Scrollable.of(). This ' 'can happen because you are using a widget that looks for a Scrollable ' 'ancestor, but no such ancestor exists.\n' 'The context used was:\n' ' $context', ), if (axis != null) ErrorHint( 'When specifying an axis, this method will only look for a Scrollable ' 'that matches the given Axis.', ), ]); } return true; }()); return scrollableState!; } /// Provides a heuristic to determine if expensive frame-bound tasks should be /// deferred for the [context] at a specific point in time. /// /// Calling this method does _not_ create a dependency on any other widget. /// This also means that the value returned is only good for the point in time /// when it is called, and callers will not get updated if the value changes. /// /// The heuristic used is determined by the [physics] of this [Scrollable] /// via [ScrollPhysics.recommendDeferredLoading]. That method is called with /// the current [ScrollPosition.activity]'s [ScrollActivity.velocity]. /// /// The optional [Axis] allows targeting of a specific [Scrollable] of that /// axis, useful when Scrollables are nested. When [axis] is provided, /// [ScrollPosition.recommendDeferredLoading] is called for the nearest /// [Scrollable] in that [Axis]. /// /// If there is no [Scrollable] in the widget tree above the [context], this /// method returns false. static bool recommendDeferredLoadingForContext( BuildContext context, { Axis? axis, }) { _ScrollableScope? widget = context .getInheritedWidgetOfExactType<_ScrollableScope>(); while (widget != null) { if (axis == null || axisDirectionToAxis(widget.scrollable.axisDirection) == axis) { return widget.position.recommendDeferredLoading(context); } context = widget.scrollable.context; widget = context.getInheritedWidgetOfExactType<_ScrollableScope>(); } return false; } /// Scrolls all scrollables that enclose the given context so as to make the /// given context visible. /// /// If a [Scrollable] enclosing the provided [BuildContext] is a /// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure /// the target is made visible. static Future ensureVisible( BuildContext context, { double alignment = 0.0, Duration duration = Duration.zero, Curve curve = Curves.ease, ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, }) { final futures = >[]; // The targetRenderObject is used to record the first target renderObject. // If there are multiple scrollable widgets nested, the targetRenderObject // is made to be as visible as possible to improve the user experience. If // the targetRenderObject is already visible, then let the outer // renderObject be as visible as possible. // // Also see https://github.com/flutter/flutter/issues/65100 RenderObject? targetRenderObject; ScrollableState? scrollable = Scrollable.maybeOf(context); while (scrollable != null) { final List> newFutures; (newFutures, scrollable) = scrollable._performEnsureVisible( context.findRenderObject()!, alignment: alignment, duration: duration, curve: curve, alignmentPolicy: alignmentPolicy, targetRenderObject: targetRenderObject, ); futures.addAll(newFutures); targetRenderObject ??= context.findRenderObject(); context = scrollable.context; scrollable = Scrollable.maybeOf(context); } if (futures.isEmpty || duration == Duration.zero) { return Future.value(); } if (futures.length == 1) { return futures.single; } return Future.wait(futures).then((List _) => null); } } // Enable Scrollable.of() to work as if ScrollableState was an inherited widget. // ScrollableState.build() always rebuilds its _ScrollableScope. class _ScrollableScope extends InheritedWidget { const _ScrollableScope({ required this.scrollable, required this.position, required super.child, }); final ScrollableState scrollable; final ScrollPosition position; @override bool updateShouldNotify(_ScrollableScope old) { return position != old.position; } } /// State object for a [Scrollable] widget. /// /// To manipulate a [Scrollable] widget's scroll position, use the object /// obtained from the [position] property. /// /// To be informed of when a [Scrollable] widget is scrolling, use a /// [NotificationListener] to listen for [ScrollNotification] notifications. /// /// This class is not intended to be subclassed. To specialize the behavior of a /// [Scrollable], provide it with a [ScrollPhysics]. class ScrollableState extends State> with TickerProviderStateMixin, RestorationMixin implements ScrollContext { // GETTERS /// The manager for this [Scrollable] widget's viewport position. /// /// To control what kind of [ScrollPosition] is created for a [Scrollable], /// provide it with custom [ScrollController] that creates the appropriate /// [ScrollPosition] in its [ScrollController.createScrollPosition] method. ScrollPosition get position => _position!; ScrollPosition? _position; /// The resolved [ScrollPhysics] of the [ScrollableState]. ScrollPhysics? get resolvedPhysics => _physics; ScrollPhysics? _physics; /// An [Offset] that represents the absolute distance from the origin, or 0, /// of the [ScrollPosition] expressed in the associated [Axis]. /// /// Used by [EdgeDraggingAutoScroller] to progress the position forward when a /// drag gesture reaches the edge of the [Viewport]. Offset get deltaToScrollOrigin => switch (axisDirection) { AxisDirection.up => Offset(0, -position.pixels), AxisDirection.down => Offset(0, position.pixels), AxisDirection.left => Offset(-position.pixels, 0), AxisDirection.right => Offset(position.pixels, 0), }; ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!; @override AxisDirection get axisDirection => widget.axisDirection; @override TickerProvider get vsync => this; @override double get devicePixelRatio => _devicePixelRatio; late double _devicePixelRatio; @override BuildContext? get notificationContext => _gestureDetectorKey.currentContext; @override BuildContext get storageContext => context; @override String? get restorationId => widget.restorationId; final _RestorableScrollOffset _persistedScrollOffset = _RestorableScrollOffset(); late ScrollBehavior _configuration; ScrollController? _fallbackScrollController; DeviceGestureSettings? _mediaQueryGestureSettings; // Only call this from places that will definitely trigger a rebuild. void _updatePosition() { _configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context); final ScrollPhysics? physicsFromWidget = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); _physics = _configuration.getScrollPhysics(context); _physics = physicsFromWidget?.applyTo(_physics) ?? _physics; final ScrollPosition? oldPosition = _position; if (oldPosition != null) { _effectiveScrollController.detach(oldPosition); // It's important that we not dispose the old position until after the // viewport has had a chance to unregister its listeners from the old // position. So, schedule a microtask to do it. scheduleMicrotask(oldPosition.dispose); } _position = _effectiveScrollController.createScrollPosition( _physics!, this, oldPosition, ); assert(_position != null); _effectiveScrollController.attach(position); } @protected @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_persistedScrollOffset, 'offset'); assert(_position != null); if (_persistedScrollOffset.value != null) { position.restoreOffset( _persistedScrollOffset.value!, initialRestore: initialRestore, ); } } @protected @override void saveOffset(double offset) { assert(debugIsSerializableForRestoration(offset)); _persistedScrollOffset.value = offset; // [saveOffset] is called after a scrolling ends and it is usually not // followed by a frame. Therefore, manually flush restoration data. ServicesBinding.instance.restorationManager.flushData(); } @protected @override void initState() { if (widget.controller == null) { _fallbackScrollController = ScrollController(); } super.initState(); } @protected @override void didChangeDependencies() { _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context); _devicePixelRatio = MediaQuery.maybeDevicePixelRatioOf(context) ?? View.of(context).devicePixelRatio; _updatePosition(); super.didChangeDependencies(); } bool _shouldUpdatePosition(Scrollable oldWidget) { if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) { return true; } if (widget.scrollBehavior != null && oldWidget.scrollBehavior != null && widget.scrollBehavior!.shouldNotify(oldWidget.scrollBehavior!)) { return true; } ScrollPhysics? newPhysics = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); ScrollPhysics? oldPhysics = oldWidget.physics ?? oldWidget.scrollBehavior?.getScrollPhysics(context); do { if (newPhysics?.runtimeType != oldPhysics?.runtimeType) { return true; } newPhysics = newPhysics?.parent; oldPhysics = oldPhysics?.parent; } while (newPhysics != null || oldPhysics != null); return widget.controller?.runtimeType != oldWidget.controller?.runtimeType; } @protected @override void didUpdateWidget(Scrollable oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { if (oldWidget.controller == null) { // The old controller was null, meaning the fallback cannot be null. // Dispose of the fallback. assert(_fallbackScrollController != null); assert(widget.controller != null); _fallbackScrollController!.detach(position); _fallbackScrollController!.dispose(); _fallbackScrollController = null; } else { // The old controller was not null, detach. oldWidget.controller?.detach(position); if (widget.controller == null) { // If the new controller is null, we need to set up the fallback // ScrollController. _fallbackScrollController = ScrollController(); } } // Attach the updated effective scroll controller. _effectiveScrollController.attach(position); } if (_shouldUpdatePosition(oldWidget)) { _updatePosition(); } } @protected @override void dispose() { if (widget.controller != null) { widget.controller!.detach(position); } else { _fallbackScrollController?.detach(position); _fallbackScrollController?.dispose(); } position.dispose(); _persistedScrollOffset.dispose(); super.dispose(); } // SEMANTICS final GlobalKey _scrollSemanticsKey = GlobalKey(); @override @protected void setSemanticsActions(Set actions) { if (_gestureDetectorKey.currentState != null) { _gestureDetectorKey.currentState!.replaceSemanticsActions(actions); } } // GESTURE RECOGNITION AND POINTER IGNORING final GlobalKey _gestureDetectorKey = GlobalKey(); final GlobalKey _ignorePointerKey = GlobalKey(); // This field is set during layout, and then reused until the next time it is set. Map _gestureRecognizers = const {}; bool _shouldIgnorePointer = false; bool? _lastCanDrag; Axis? _lastAxisDirection; @override @protected void setCanDrag(bool value) { if (value == _lastCanDrag && (!value || widget.axis == _lastAxisDirection)) { return; } if (!value) { _gestureRecognizers = const {}; // Cancel the active hold/drag (if any) because the gesture recognizers // will soon be disposed by our RawGestureDetector, and we won't be // receiving pointer up events to cancel the hold/drag. _handleDragCancel(); } else { switch (widget.axis) { case Axis.vertical: _gestureRecognizers = { VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< VerticalDragGestureRecognizer >( () => VerticalDragGestureRecognizer( supportedDevices: _configuration.dragDevices, ), (VerticalDragGestureRecognizer instance) { instance ..onDown = _handleDragDown ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel ..minFlingDistance = _physics?.minFlingDistance ..minFlingVelocity = _physics?.minFlingVelocity ..maxFlingVelocity = _physics?.maxFlingVelocity ..velocityTrackerBuilder = _configuration .velocityTrackerBuilder(context) ..dragStartBehavior = widget.dragStartBehavior ..multitouchDragStrategy = _configuration .getMultitouchDragStrategy(context) ..gestureSettings = _mediaQueryGestureSettings ..supportedDevices = _configuration.dragDevices; }, ), }; case Axis.horizontal: _gestureRecognizers = { T: GestureRecognizerFactoryWithHandlers( widget.horizontalDragGestureRecognizer, (T instance) { instance ..onDown = _handleDragDown ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel ..minFlingDistance = _physics?.minFlingDistance ..minFlingVelocity = _physics?.minFlingVelocity ..maxFlingVelocity = _physics?.maxFlingVelocity ..velocityTrackerBuilder = _configuration .velocityTrackerBuilder(context) ..dragStartBehavior = widget.dragStartBehavior ..multitouchDragStrategy = _configuration .getMultitouchDragStrategy(context) ..gestureSettings = _mediaQueryGestureSettings ..supportedDevices = _configuration.dragDevices; }, ), }; } } _lastCanDrag = value; _lastAxisDirection = widget.axis; if (_gestureDetectorKey.currentState != null) { _gestureDetectorKey.currentState!.replaceGestureRecognizers( _gestureRecognizers, ); } } @override @protected void setIgnorePointer(bool value) { if (_shouldIgnorePointer == value) { return; } _shouldIgnorePointer = value; if (_ignorePointerKey.currentContext != null) { final renderBox = _ignorePointerKey.currentContext!.findRenderObject()! as RenderIgnorePointer; renderBox.ignoring = _shouldIgnorePointer; } } // TOUCH HANDLERS Drag? _drag; ScrollHoldController? _hold; void _handleDragDown(DragDownDetails details) { assert(_drag == null); assert(_hold == null); _hold = position.hold(_disposeHold); } void _handleDragStart(DragStartDetails details) { // It's possible for _hold to become null between _handleDragDown and // _handleDragStart, for example if some user code calls jumpTo or otherwise // triggers a new activity to begin. assert(_drag == null); _drag = position.drag(details, _disposeDrag); assert(_drag != null); // _hold might be non-null if the scroll position is currently animating. if (_hold != null) { _disposeHold(); } } void _handleDragUpdate(DragUpdateDetails details) { // _drag might be null if the drag activity ended and called _disposeDrag. assert(_hold == null || _drag == null); _drag?.update(details); } void _handleDragEnd(DragEndDetails details) { // _drag might be null if the drag activity ended and called _disposeDrag. assert(_hold == null || _drag == null); _drag?.end(details); assert(_drag == null); } void _handleDragCancel() { if (_gestureDetectorKey.currentContext == null) { // The cancel was caused by the GestureDetector getting disposed, which // means we will get disposed momentarily as well and shouldn't do // any work. return; } // _hold might be null if the drag started. // _drag might be null if the drag activity ended and called _disposeDrag. assert(_hold == null || _drag == null); _hold?.cancel(); _drag?.cancel(); assert(_hold == null); assert(_drag == null); } void _disposeHold() { _hold = null; } void _disposeDrag() { _drag = null; } // SCROLL WHEEL // Returns the offset that should result from applying [event] to the current // position, taking min/max scroll extent into account. double _targetScrollOffsetForPointerScroll(double delta) { return math.min( math.max(position.pixels + delta, position.minScrollExtent), position.maxScrollExtent, ); } // Returns the delta that should result from applying [event] with axis, // direction, and any modifiers specified by the ScrollBehavior taken into // account. double _pointerSignalEventDelta(PointerScrollEvent event) { final Set pressed = HardwareKeyboard.instance.logicalKeysPressed; final bool flipAxes = pressed.any(_configuration.pointerAxisModifiers.contains) && // Axes are only flipped for physical mouse wheel input. // On some platforms, like web, trackpad input is handled through pointer // signals, but should not be included in this axis modifying behavior. // This is because on a trackpad, all directional axes are available to // the user, while mouse scroll wheels typically are restricted to one // axis. event.kind == PointerDeviceKind.mouse; final Axis axis = flipAxes ? flipAxis(widget.axis) : widget.axis; final double delta = switch (axis) { Axis.horizontal => event.scrollDelta.dx, Axis.vertical => event.scrollDelta.dy, }; return axisDirectionIsReversed(widget.axisDirection) ? -delta : delta; } void _receivedPointerSignal(PointerSignalEvent event) { if (event is PointerScrollEvent && _position != null) { if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) { // The handler won't use the `event`, so allow the platform to trigger // any default native actions. event.respond(allowPlatformDefault: true); return; } final double delta = _pointerSignalEventDelta(event); final double targetScrollOffset = _targetScrollOffsetForPointerScroll( delta, ); // Only express interest in the event if it would actually result in a scroll. if (delta != 0.0 && targetScrollOffset != position.pixels) { GestureBinding.instance.pointerSignalResolver.register( event, _handlePointerScroll, ); return; } // The `event` won't result in a scroll, so allow the platform to trigger // any default native actions. event.respond(allowPlatformDefault: true); } else if (event is PointerScrollInertiaCancelEvent) { position.pointerScroll(0); // Don't use the pointer signal resolver, all hit-tested scrollables should stop. } } void _handlePointerScroll(PointerEvent event) { assert(event is PointerScrollEvent); final double delta = _pointerSignalEventDelta(event as PointerScrollEvent); final double targetScrollOffset = _targetScrollOffsetForPointerScroll( delta, ); if (delta != 0.0 && targetScrollOffset != position.pixels) { position.pointerScroll(delta); } } bool _handleScrollMetricsNotification( ScrollMetricsNotification notification, ) { if (notification.depth == 0) { final RenderObject? scrollSemanticsRenderObject = _scrollSemanticsKey .currentContext ?.findRenderObject(); if (scrollSemanticsRenderObject != null) { scrollSemanticsRenderObject.markNeedsSemanticsUpdate(); } } return false; } Widget _buildChrome(BuildContext context, Widget child) { final details = ScrollableDetails( direction: widget.axisDirection, controller: _effectiveScrollController, decorationClipBehavior: widget.clipBehavior, ); return _configuration.buildScrollbar( context, _configuration.buildOverscrollIndicator(context, child, details), details, ); } // DESCRIPTION @protected @override Widget build(BuildContext context) { assert(_position != null); // _ScrollableScope must be placed above the BuildContext returned by notificationContext // so that we can get this ScrollableState by doing the following: // // ScrollNotification notification; // Scrollable.of(notification.context) // // Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope // must be placed above the widget using it: RawGestureDetector Widget result = _ScrollableScope( scrollable: this, position: position, child: Listener( onPointerSignal: _receivedPointerSignal, child: RawGestureDetector( key: _gestureDetectorKey, gestures: _gestureRecognizers, behavior: widget.hitTestBehavior, excludeFromSemantics: widget.excludeFromSemantics, child: Semantics( explicitChildNodes: !widget.excludeFromSemantics, child: IgnorePointer( key: _ignorePointerKey, ignoring: _shouldIgnorePointer, child: widget.viewportBuilder(context, position), ), ), ), ), ); if (!widget.excludeFromSemantics) { result = NotificationListener( onNotification: _handleScrollMetricsNotification, child: _ScrollSemantics( key: _scrollSemanticsKey, position: position, allowImplicitScrolling: _physics!.allowImplicitScrolling, axis: widget.axis, semanticChildCount: widget.semanticChildCount, child: result, ), ); } result = _buildChrome(context, result); // Selection is only enabled when there is a parent registrar. final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); if (registrar != null) { result = _ScrollableSelectionHandler( state: this, position: position, registrar: registrar, child: result, ); } return result; } // Returns the Future from calling ensureVisible for the ScrollPosition, as // as well as this ScrollableState instance so its context can be used to // check for other ancestor Scrollables in executing ensureVisible. _EnsureVisibleResults _performEnsureVisible( RenderObject object, { double alignment = 0.0, Duration duration = Duration.zero, Curve curve = Curves.ease, ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, RenderObject? targetRenderObject, }) { final Future ensureVisibleFuture = position.ensureVisible( object, alignment: alignment, duration: duration, curve: curve, alignmentPolicy: alignmentPolicy, targetRenderObject: targetRenderObject, ); return (>[ensureVisibleFuture], this); } @protected @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('position', _position)) ..add(DiagnosticsProperty('effective physics', _physics)); } } /// A widget to handle selection for a scrollable. /// /// This widget registers itself to the [registrar] and uses /// [SelectionContainer] to collect selectables from its subtree. class _ScrollableSelectionHandler extends StatefulWidget { const _ScrollableSelectionHandler({ required this.state, required this.position, required this.registrar, required this.child, }); final ScrollableState state; final ScrollPosition position; final Widget child; final SelectionRegistrar registrar; @override _ScrollableSelectionHandlerState createState() => _ScrollableSelectionHandlerState(); } class _ScrollableSelectionHandlerState extends State<_ScrollableSelectionHandler> { late _ScrollableSelectionContainerDelegate _selectionDelegate; @override void initState() { super.initState(); _selectionDelegate = _ScrollableSelectionContainerDelegate( state: widget.state, position: widget.position, ); } @override void didUpdateWidget(_ScrollableSelectionHandler oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.position != widget.position) { _selectionDelegate.position = widget.position; } } @override void dispose() { _selectionDelegate.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SelectionContainer( registrar: widget.registrar, delegate: _selectionDelegate, child: widget.child, ); } } /// This updater handles the case where the selectables change frequently, and /// it optimizes toward scrolling updates. /// /// It keeps track of the drag start offset relative to scroll origin for every /// selectable. The records are used to determine whether the selection is up to /// date with the scroll position when it sends the drag update event to a /// selectable. class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate { _ScrollableSelectionContainerDelegate({ required this.state, required ScrollPosition position, }) : _position = position, _autoScroller = EdgeDraggingAutoScroller( state, velocityScalar: _kDefaultSelectToScrollVelocityScalar, ) { _position.addListener(_scheduleLayoutChange); } // Pointer drag is a single point, it should not have a size. static const double _kDefaultDragTargetSize = 0; // An eye-balled value for a smooth scrolling speed. static const double _kDefaultSelectToScrollVelocityScalar = 30; final ScrollableState state; final EdgeDraggingAutoScroller _autoScroller; bool _scheduledLayoutChange = false; Offset? _currentDragStartRelatedToOrigin; Offset? _currentDragEndRelatedToOrigin; // The scrollable only auto scrolls if the selection starts in the scrollable. bool _selectionStartsInScrollable = false; ScrollPosition get position => _position; ScrollPosition _position; set position(ScrollPosition other) { if (other == _position) { return; } _position.removeListener(_scheduleLayoutChange); _position = other; _position.addListener(_scheduleLayoutChange); } // The layout will only be updated a frame later than position changes. // Schedule PostFrameCallback to capture the accurate layout. void _scheduleLayoutChange() { if (_scheduledLayoutChange) { return; } _scheduledLayoutChange = true; SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { if (!_scheduledLayoutChange) { return; } _scheduledLayoutChange = false; layoutDidChange(); }, debugLabel: 'ScrollableSelectionContainer.layoutDidChange'); } /// Stores the scroll offset when a scrollable receives the last /// [SelectionEdgeUpdateEvent]. /// /// The stored scroll offset may be null if a scrollable never receives a /// [SelectionEdgeUpdateEvent]. /// /// When a new [SelectionEdgeUpdateEvent] is dispatched to a selectable, this /// updater checks the current scroll offset against the one stored in these /// records. If the scroll offset is different, it synthesizes an opposite /// [SelectionEdgeUpdateEvent] and dispatches the event before dispatching the /// new event. /// /// For example, if a selectable receives an end [SelectionEdgeUpdateEvent] /// and its scroll offset in the records is different from the current value, /// it synthesizes a start [SelectionEdgeUpdateEvent] and dispatches it before /// dispatching the original end [SelectionEdgeUpdateEvent]. final Map _selectableStartEdgeUpdateRecords = {}; final Map _selectableEndEdgeUpdateRecords = {}; @override void didChangeSelectables() { final Set selectableSet = selectables.toSet(); _selectableStartEdgeUpdateRecords.removeWhere( (Selectable key, double value) => !selectableSet.contains(key), ); _selectableEndEdgeUpdateRecords.removeWhere( (Selectable key, double value) => !selectableSet.contains(key), ); super.didChangeSelectables(); } @override SelectionResult handleClearSelection(ClearSelectionEvent event) { _selectableStartEdgeUpdateRecords.clear(); _selectableEndEdgeUpdateRecords.clear(); _currentDragStartRelatedToOrigin = null; _currentDragEndRelatedToOrigin = null; _selectionStartsInScrollable = false; return super.handleClearSelection(event); } @override SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { if (_currentDragEndRelatedToOrigin == null && _currentDragStartRelatedToOrigin == null) { assert(!_selectionStartsInScrollable); _selectionStartsInScrollable = _globalPositionInScrollable( event.globalPosition, ); } final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); if (event.type == SelectionEventType.endEdgeUpdate) { _currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin( event.globalPosition, ); final Offset endOffset = _currentDragEndRelatedToOrigin!.translate( -deltaToOrigin.dx, -deltaToOrigin.dy, ); event = SelectionEdgeUpdateEvent.forEnd( globalPosition: endOffset, granularity: event.granularity, ); } else { _currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin( event.globalPosition, ); final Offset startOffset = _currentDragStartRelatedToOrigin!.translate( -deltaToOrigin.dx, -deltaToOrigin.dy, ); event = SelectionEdgeUpdateEvent.forStart( globalPosition: startOffset, granularity: event.granularity, ); } final SelectionResult result = super.handleSelectionEdgeUpdate(event); // Result may be pending if one of the selectable child is also a scrollable. // In that case, the parent scrollable needs to wait for the child to finish // scrolling. if (result == SelectionResult.pending) { _autoScroller.stopAutoScroll(); return result; } if (_selectionStartsInScrollable) { _autoScroller.startAutoScrollIfNecessary(_dragTargetFromEvent(event)); if (_autoScroller.scrolling) { return SelectionResult.pending; } } return result; } Offset _inferPositionRelatedToOrigin(Offset globalPosition) { final box = state.context.findRenderObject()! as RenderBox; final Offset localPosition = box.globalToLocal(globalPosition); if (!_selectionStartsInScrollable) { // If the selection starts outside of the scrollable, selecting across the // scrollable boundary will act as selecting the entire content in the // scrollable. This logic move the offset to the 0.0 or infinity to cover // the entire content if the input position is outside of the scrollable. if (localPosition.dy < 0 || localPosition.dx < 0) { return box.localToGlobal(Offset.zero); } if (localPosition.dy > box.size.height || localPosition.dx > box.size.width) { return Offset.infinite; } } final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); return box.localToGlobal( localPosition.translate(deltaToOrigin.dx, deltaToOrigin.dy), ); } /// Infers the [_currentDragStartRelatedToOrigin] and /// [_currentDragEndRelatedToOrigin] from the geometry. /// /// This method is called after a select word and select all event where the /// selection is triggered by none drag events. The /// [_currentDragStartRelatedToOrigin] and [_currentDragEndRelatedToOrigin] /// are essential to handle future [SelectionEdgeUpdateEvent]s. void _updateDragLocationsFromGeometries({ bool forceUpdateStart = true, bool forceUpdateEnd = true, }) { final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final box = state.context.findRenderObject()! as RenderBox; final Matrix4 transform = box.getTransformTo(null); if (currentSelectionStartIndex != -1 && (_currentDragStartRelatedToOrigin == null || forceUpdateStart)) { final SelectionGeometry geometry = selectables[currentSelectionStartIndex].value; assert(geometry.hasSelection); final SelectionPoint start = geometry.startSelectionPoint!; final Matrix4 childTransform = selectables[currentSelectionStartIndex] .getTransformTo(box); final Offset localDragStart = MatrixUtils.transformPoint( childTransform, start.localPosition + Offset(0, -start.lineHeight / 2), ); _currentDragStartRelatedToOrigin = MatrixUtils.transformPoint( transform, localDragStart + deltaToOrigin, ); } if (currentSelectionEndIndex != -1 && (_currentDragEndRelatedToOrigin == null || forceUpdateEnd)) { final SelectionGeometry geometry = selectables[currentSelectionEndIndex].value; assert(geometry.hasSelection); final SelectionPoint end = geometry.endSelectionPoint!; final Matrix4 childTransform = selectables[currentSelectionEndIndex] .getTransformTo(box); final Offset localDragEnd = MatrixUtils.transformPoint( childTransform, end.localPosition + Offset(0, -end.lineHeight / 2), ); _currentDragEndRelatedToOrigin = MatrixUtils.transformPoint( transform, localDragEnd + deltaToOrigin, ); } } @override SelectionResult handleSelectAll(SelectAllSelectionEvent event) { assert(!_selectionStartsInScrollable); final SelectionResult result = super.handleSelectAll(event); assert( (currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1), ); if (currentSelectionStartIndex != -1) { _updateDragLocationsFromGeometries(); } return result; } @override SelectionResult handleSelectWord(SelectWordSelectionEvent event) { _selectionStartsInScrollable = _globalPositionInScrollable( event.globalPosition, ); final SelectionResult result = super.handleSelectWord(event); _updateDragLocationsFromGeometries(); return result; } @override SelectionResult handleGranularlyExtendSelection( GranularlyExtendSelectionEvent event, ) { final SelectionResult result = super.handleGranularlyExtendSelection(event); // The selection geometry may not have the accurate offset for the edges // that are outside of the viewport whose transform may not be valid. Only // the edge this event is updating is sure to be accurate. _updateDragLocationsFromGeometries( forceUpdateStart: !event.isEnd, forceUpdateEnd: event.isEnd, ); if (_selectionStartsInScrollable) { _jumpToEdge(event.isEnd); } return result; } @override SelectionResult handleDirectionallyExtendSelection( DirectionallyExtendSelectionEvent event, ) { final SelectionResult result = super.handleDirectionallyExtendSelection( event, ); // The selection geometry may not have the accurate offset for the edges // that are outside of the viewport whose transform may not be valid. Only // the edge this event is updating is sure to be accurate. _updateDragLocationsFromGeometries( forceUpdateStart: !event.isEnd, forceUpdateEnd: event.isEnd, ); if (_selectionStartsInScrollable) { _jumpToEdge(event.isEnd); } return result; } void _jumpToEdge(bool isExtent) { final Selectable selectable; final double? lineHeight; final SelectionPoint? edge; if (isExtent) { selectable = selectables[currentSelectionEndIndex]; edge = selectable.value.endSelectionPoint; lineHeight = selectable.value.endSelectionPoint!.lineHeight; } else { selectable = selectables[currentSelectionStartIndex]; edge = selectable.value.startSelectionPoint; lineHeight = selectable.value.startSelectionPoint?.lineHeight; } if (lineHeight == null || edge == null) { return; } final scrollableBox = state.context.findRenderObject()! as RenderBox; final Matrix4 transform = selectable.getTransformTo(scrollableBox); final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint( transform, edge.localPosition, ); final scrollableRect = Rect.fromLTRB( 0, 0, scrollableBox.size.width, scrollableBox.size.height, ); switch (state.axisDirection) { case AxisDirection.up: final double edgeBottom = edgeOffsetInScrollableCoordinates.dy; final double edgeTop = edgeOffsetInScrollableCoordinates.dy - lineHeight; if (edgeBottom >= scrollableRect.bottom && edgeTop <= scrollableRect.top) { return; } if (edgeBottom > scrollableRect.bottom) { position.jumpTo(position.pixels + scrollableRect.bottom - edgeBottom); return; } if (edgeTop < scrollableRect.top) { position.jumpTo(position.pixels + scrollableRect.top - edgeTop); } return; case AxisDirection.right: final double edge = edgeOffsetInScrollableCoordinates.dx; if (edge >= scrollableRect.right && edge <= scrollableRect.left) { return; } if (edge > scrollableRect.right) { position.jumpTo(position.pixels + edge - scrollableRect.right); return; } if (edge < scrollableRect.left) { position.jumpTo(position.pixels + edge - scrollableRect.left); } return; case AxisDirection.down: final double edgeBottom = edgeOffsetInScrollableCoordinates.dy; final double edgeTop = edgeOffsetInScrollableCoordinates.dy - lineHeight; if (edgeBottom >= scrollableRect.bottom && edgeTop <= scrollableRect.top) { return; } if (edgeBottom > scrollableRect.bottom) { position.jumpTo(position.pixels + edgeBottom - scrollableRect.bottom); return; } if (edgeTop < scrollableRect.top) { position.jumpTo(position.pixels + edgeTop - scrollableRect.top); } return; case AxisDirection.left: final double edge = edgeOffsetInScrollableCoordinates.dx; if (edge >= scrollableRect.right && edge <= scrollableRect.left) { return; } if (edge > scrollableRect.right) { position.jumpTo(position.pixels + scrollableRect.right - edge); return; } if (edge < scrollableRect.left) { position.jumpTo(position.pixels + scrollableRect.left - edge); } return; } } bool _globalPositionInScrollable(Offset globalPosition) { final box = state.context.findRenderObject()! as RenderBox; final Offset localPosition = box.globalToLocal(globalPosition); final rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height); return rect.contains(localPosition); } Rect _dragTargetFromEvent(SelectionEdgeUpdateEvent event) { return Rect.fromCenter( center: event.globalPosition, width: _kDefaultDragTargetSize, height: _kDefaultDragTargetSize, ); } @override SelectionResult dispatchSelectionEventToChild( Selectable selectable, SelectionEvent event, ) { switch (event.type) { case SelectionEventType.startEdgeUpdate: _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; ensureChildUpdated(selectable); case SelectionEventType.endEdgeUpdate: _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; ensureChildUpdated(selectable); case SelectionEventType.granularlyExtendSelection: case SelectionEventType.directionallyExtendSelection: ensureChildUpdated(selectable); _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; case SelectionEventType.clear: _selectableEndEdgeUpdateRecords.remove(selectable); _selectableStartEdgeUpdateRecords.remove(selectable); case SelectionEventType.selectAll: case SelectionEventType.selectWord: case SelectionEventType.selectParagraph: _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; } return super.dispatchSelectionEventToChild(selectable, event); } @override void ensureChildUpdated(Selectable selectable) { final double newRecord = state.position.pixels; final double? previousStartRecord = _selectableStartEdgeUpdateRecords[selectable]; if (_currentDragStartRelatedToOrigin != null && (previousStartRecord == null || (newRecord - previousStartRecord).abs() > precisionErrorTolerance)) { // Make sure the selectable has up to date events. final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final Offset startOffset = _currentDragStartRelatedToOrigin!.translate( -deltaToOrigin.dx, -deltaToOrigin.dy, ); selectable.dispatchSelectionEvent( SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset), ); // Make sure we track that we have synthesized a start event for this selectable, // so we don't synthesize events unnecessarily. _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; } final double? previousEndRecord = _selectableEndEdgeUpdateRecords[selectable]; if (_currentDragEndRelatedToOrigin != null && (previousEndRecord == null || (newRecord - previousEndRecord).abs() > precisionErrorTolerance)) { // Make sure the selectable has up to date events. final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final Offset endOffset = _currentDragEndRelatedToOrigin!.translate( -deltaToOrigin.dx, -deltaToOrigin.dy, ); selectable.dispatchSelectionEvent( SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset), ); // Make sure we track that we have synthesized an end event for this selectable, // so we don't synthesize events unnecessarily. _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; } } @override void dispose() { _selectableStartEdgeUpdateRecords.clear(); _selectableEndEdgeUpdateRecords.clear(); _scheduledLayoutChange = false; _autoScroller.stopAutoScroll(); super.dispose(); } } Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) { return switch (scrollableState.axisDirection) { AxisDirection.up => Offset(0, -scrollableState.position.pixels), AxisDirection.down => Offset(0, scrollableState.position.pixels), AxisDirection.left => Offset(-scrollableState.position.pixels, 0), AxisDirection.right => Offset(scrollableState.position.pixels, 0), }; } /// With [_ScrollSemantics] certain child [SemanticsNode]s can be /// excluded from the scrollable area for semantics purposes. /// /// Nodes, that are to be excluded, have to be tagged with /// [RenderViewport.excludeFromScrolling] and the [RenderAbstractViewport] in /// use has to add the [RenderViewport.useTwoPaneSemantics] tag to its /// [SemanticsConfiguration] by overriding /// [RenderObject.describeSemanticsConfiguration]. /// /// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport, /// two semantics nodes will be used to represent the [Scrollable]: The outer /// node will contain all children, that are excluded from scrolling. The inner /// node, which is annotated with the scrolling actions, will house the /// scrollable children. class _ScrollSemantics extends SingleChildRenderObjectWidget { const _ScrollSemantics({ super.key, required this.position, required this.allowImplicitScrolling, required this.axis, required this.semanticChildCount, super.child, }) : assert(semanticChildCount == null || semanticChildCount >= 0); final ScrollPosition position; final bool allowImplicitScrolling; final int? semanticChildCount; final Axis axis; @override _RenderScrollSemantics createRenderObject(BuildContext context) { return _RenderScrollSemantics( position: position, allowImplicitScrolling: allowImplicitScrolling, semanticChildCount: semanticChildCount, axis: axis, ); } @override void updateRenderObject( BuildContext context, _RenderScrollSemantics renderObject, ) { renderObject ..allowImplicitScrolling = allowImplicitScrolling ..axis = axis ..position = position ..semanticChildCount = semanticChildCount; } } class _RenderScrollSemantics extends RenderProxyBox { _RenderScrollSemantics({ required ScrollPosition position, required bool allowImplicitScrolling, required this.axis, required int? semanticChildCount, RenderBox? child, }) : _position = position, _allowImplicitScrolling = allowImplicitScrolling, _semanticChildCount = semanticChildCount, super(child) { position.addListener(markNeedsSemanticsUpdate); } /// Whether this render object is excluded from the semantic tree. ScrollPosition get position => _position; ScrollPosition _position; set position(ScrollPosition value) { if (value == _position) { return; } _position.removeListener(markNeedsSemanticsUpdate); _position = value; _position.addListener(markNeedsSemanticsUpdate); markNeedsSemanticsUpdate(); } /// Whether this node can be scrolled implicitly. bool get allowImplicitScrolling => _allowImplicitScrolling; bool _allowImplicitScrolling; set allowImplicitScrolling(bool value) { if (value == _allowImplicitScrolling) { return; } _allowImplicitScrolling = value; markNeedsSemanticsUpdate(); } Axis axis; int? get semanticChildCount => _semanticChildCount; int? _semanticChildCount; set semanticChildCount(int? value) { if (value == semanticChildCount) { return; } _semanticChildCount = value; markNeedsSemanticsUpdate(); } void _onScrollToOffset(Offset targetOffset) { final double offset = switch (axis) { Axis.horizontal => targetOffset.dx, Axis.vertical => targetOffset.dy, }; _position.jumpTo(offset); } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); config.isSemanticBoundary = true; if (position.haveDimensions) { config ..hasImplicitScrolling = allowImplicitScrolling ..scrollPosition = _position.pixels ..scrollExtentMax = _position.maxScrollExtent ..scrollExtentMin = _position.minScrollExtent ..scrollChildCount = semanticChildCount; if (position.maxScrollExtent > position.minScrollExtent && allowImplicitScrolling) { config.onScrollToOffset = _onScrollToOffset; } } } SemanticsNode? _innerNode; @override void assembleSemanticsNode( SemanticsNode node, SemanticsConfiguration config, Iterable children, ) { if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) { _innerNode = null; super.assembleSemanticsNode(node, config, children); return; } (_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect; int? firstVisibleIndex; final excluded = [_innerNode!]; final included = []; for (final child in children) { assert(child.isTagged(RenderViewport.useTwoPaneSemantics)); if (child.isTagged(RenderViewport.excludeFromScrolling)) { excluded.add(child); } else { if (!child.flagsCollection.isHidden) { firstVisibleIndex ??= child.indexInParent; } included.add(child); } } config.scrollIndex = firstVisibleIndex; node.updateWith(config: null, childrenInInversePaintOrder: excluded); _innerNode!.updateWith( config: config, childrenInInversePaintOrder: included, ); } @override void clearSemantics() { super.clearSemantics(); _innerNode = null; } } // Not using a RestorableDouble because we want to allow null values and override // [enabled]. class _RestorableScrollOffset extends RestorableValue { @override double? createDefaultValue() => null; @override void didUpdateValue(double? oldValue) { notifyListeners(); } @override double fromPrimitives(Object? data) { return data! as double; } @override Object? toPrimitives() { return value; } @override bool get enabled => value != null; } ================================================ FILE: lib/common/widgets/flutter/page/scrollable_helpers.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/material.dart'; /// /// @docImport 'overscroll_indicator.dart'; /// @docImport 'viewport.dart'; // ignore_for_file: dangling_library_doc_comments import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide EdgeDraggingAutoScroller, Scrollable, ScrollableState; /// An auto scroller that scrolls the [scrollable] if a drag gesture drags close /// to its edge. /// /// The scroll velocity is controlled by the [velocityScalar]: /// /// velocity = (distance of overscroll) * [velocityScalar]. class EdgeDraggingAutoScroller { /// Creates a auto scroller that scrolls the [scrollable]. EdgeDraggingAutoScroller( this.scrollable, { this.onScrollViewScrolled, required this.velocityScalar, }); /// The [Scrollable] this auto scroller is scrolling. final ScrollableState scrollable; /// Called when a scroll view is scrolled. /// /// The scroll view may be scrolled multiple times in a row until the drag /// target no longer triggers the auto scroll. This callback will be called /// in between each scroll. final VoidCallback? onScrollViewScrolled; /// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar} /// The velocity scalar per pixel over scroll. /// /// It represents how the velocity scale with the over scroll distance. The /// auto-scroll velocity = (distance of overscroll) * velocityScalar. /// {@endtemplate} final double velocityScalar; late Rect _dragTargetRelatedToScrollOrigin; /// Whether the auto scroll is in progress. bool get scrolling => _scrolling; bool _scrolling = false; double _offsetExtent(Offset offset, Axis scrollDirection) { return switch (scrollDirection) { Axis.horizontal => offset.dx, Axis.vertical => offset.dy, }; } double _sizeExtent(Size size, Axis scrollDirection) { return switch (scrollDirection) { Axis.horizontal => size.width, Axis.vertical => size.height, }; } AxisDirection get _axisDirection => scrollable.axisDirection; Axis get _scrollDirection => axisDirectionToAxis(_axisDirection); /// Starts the auto scroll if the [dragTarget] is close to the edge. /// /// The scroll starts to scroll the [scrollable] if the target rect is close /// to the edge of the [scrollable]; otherwise, it remains stationary. /// /// If the scrollable is already scrolling, calling this method updates the /// previous dragTarget to the new value and continues scrolling if necessary. void startAutoScrollIfNecessary(Rect dragTarget) { final Offset deltaToOrigin = scrollable.deltaToScrollOrigin; _dragTargetRelatedToScrollOrigin = dragTarget.translate( deltaToOrigin.dx, deltaToOrigin.dy, ); if (_scrolling) { // The change will be picked up in the next scroll. return; } assert(!_scrolling); _scroll(); } /// Stop any ongoing auto scrolling. void stopAutoScroll() { _scrolling = false; } Future _scroll() async { final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox; final Matrix4 transform = scrollRenderBox.getTransformTo(null); final Rect globalRect = MatrixUtils.transformRect( transform, Rect.fromLTRB( 0, 0, scrollRenderBox.size.width, scrollRenderBox.size.height, ), ); final Rect transformedDragTarget = MatrixUtils.transformRect( transform, _dragTargetRelatedToScrollOrigin, ); assert( (globalRect.size.width + precisionErrorTolerance) >= transformedDragTarget.size.width && (globalRect.size.height + precisionErrorTolerance) >= transformedDragTarget.size.height, 'Drag target size is larger than scrollable size, which may cause bouncing', ); _scrolling = true; double? newOffset; const overDragMax = 20.0; final Offset deltaToOrigin = scrollable.deltaToScrollOrigin; final Offset viewportOrigin = globalRect.topLeft.translate( deltaToOrigin.dx, deltaToOrigin.dy, ); final double viewportStart = _offsetExtent( viewportOrigin, _scrollDirection, ); final double viewportEnd = viewportStart + _sizeExtent(globalRect.size, _scrollDirection); final double proxyStart = _offsetExtent( _dragTargetRelatedToScrollOrigin.topLeft, _scrollDirection, ); final double proxyEnd = _offsetExtent( _dragTargetRelatedToScrollOrigin.bottomRight, _scrollDirection, ); switch (_axisDirection) { case AxisDirection.up: case AxisDirection.left: if (proxyEnd > viewportEnd && scrollable.position.pixels > scrollable.position.minScrollExtent) { final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax); newOffset = math.max( scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag, ); } else if (proxyStart < viewportStart && scrollable.position.pixels < scrollable.position.maxScrollExtent) { final double overDrag = math.min( viewportStart - proxyStart, overDragMax, ); newOffset = math.min( scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag, ); } case AxisDirection.right: case AxisDirection.down: if (proxyStart < viewportStart && scrollable.position.pixels > scrollable.position.minScrollExtent) { final double overDrag = math.min( viewportStart - proxyStart, overDragMax, ); newOffset = math.max( scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag, ); } else if (proxyEnd > viewportEnd && scrollable.position.pixels < scrollable.position.maxScrollExtent) { final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax); newOffset = math.min( scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag, ); } } if (newOffset == null || (newOffset - scrollable.position.pixels).abs() < 1.0) { // Drag should not trigger scroll. _scrolling = false; return; } final duration = Duration(milliseconds: (1000 / velocityScalar).round()); await scrollable.position.animateTo( newOffset, duration: duration, curve: Curves.linear, ); onScrollViewScrolled?.call(); if (_scrolling) { await _scroll(); } } } ================================================ FILE: lib/common/widgets/flutter/page/tabs.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; import 'package:flutter/material.dart' hide TabBarView, PageView; /// A page view that displays the widget which corresponds to the currently /// selected tab. /// /// This widget is typically used in conjunction with a [TabBar]. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40} /// /// If a [TabController] is not provided, then there must be a [DefaultTabController] /// ancestor. /// /// The tab controller's [TabController.length] must equal the length of the /// [children] list and the length of the [TabBar.tabs] list. /// /// To see a sample implementation, visit the [TabController] documentation. class TabBarView extends StatefulWidget { /// Creates a page view with one child per tab. /// /// The length of [children] must be the same as the [controller]'s length. const TabBarView({ super.key, required this.children, this.controller, this.physics, this.dragStartBehavior = DragStartBehavior.start, this.viewportFraction = 1.0, this.clipBehavior = Clip.hardEdge, required this.horizontalDragGestureRecognizer, }); final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController? controller; /// One widget per tab. /// /// Its length must match the length of the [TabBar.tabs] /// list, as well as the [controller]'s [TabController.length]. final List children; /// How the page view should respond to user input. /// /// For example, determines how the page view continues to animate after the /// user stops dragging the page view. /// /// The physics are modified to snap to page boundaries using /// [PageScrollPhysics] prior to being used. /// /// Defaults to matching platform conventions. final ScrollPhysics? physics; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.pageview.viewportFraction} final double viewportFraction; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; @override State> createState() => _TabBarViewState(); } class _TabBarViewState extends State> { TabController? _controller; PageController? _pageController; late List _childrenWithKey; int? _currentIndex; int _warpUnderwayCount = 0; int _scrollUnderwayCount = 0; bool _debugHasScheduledValidChildrenCountCheck = false; // If the TabBarView is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController? newController = widget.controller ?? DefaultTabController.maybeOf(context); assert(() { if (newController == null) { throw FlutterError( 'No TabController for ${widget.runtimeType}.\n' 'When creating a ${widget.runtimeType}, you must either provide an explicit ' 'TabController using the "controller" property, or you must ensure that there ' 'is a DefaultTabController above the ${widget.runtimeType}.\n' 'In this case, there was neither an explicit controller nor a default controller.', ); } return true; }()); if (newController == _controller) { return; } if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = newController; if (_controller != null) { _controller!.animation!.addListener(_handleTabControllerAnimationTick); } } void _jumpToPage(int page) { _warpUnderwayCount += 1; _pageController!.jumpToPage(page); _warpUnderwayCount -= 1; } Future _animateToPage( int page, { required Duration duration, required Curve curve, }) async { _warpUnderwayCount += 1; await _pageController!.animateToPage( page, duration: duration, curve: curve, ); _warpUnderwayCount -= 1; } @override void initState() { super.initState(); _updateChildren(); } @override void didChangeDependencies() { super.didChangeDependencies(); _updateTabController(); _currentIndex = _controller!.index; if (_pageController == null) { _pageController = PageController( initialPage: _currentIndex!, viewportFraction: widget.viewportFraction, ); } else { _pageController!.jumpToPage(_currentIndex!); } } @override void didUpdateWidget(TabBarView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _updateTabController(); _currentIndex = _controller!.index; _jumpToPage(_currentIndex!); } if (widget.viewportFraction != oldWidget.viewportFraction) { _pageController?.dispose(); _pageController = PageController( initialPage: _currentIndex!, viewportFraction: widget.viewportFraction, ); } // While a warp is under way, we stop updating the tab page contents. // This is tracked in https://github.com/flutter/flutter/issues/31269. if (widget.children != oldWidget.children && _warpUnderwayCount == 0) { _updateChildren(); } } @override void dispose() { if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = null; _pageController?.dispose(); // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } void _updateChildren() { _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList( widget.children.map((Widget child) { return Semantics(role: .tabPanel, child: child); }).toList(), ); } void _handleTabControllerAnimationTick() { if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) { return; } // This widget is driving the controller's animation. if (_controller!.index != _currentIndex) { _currentIndex = _controller!.index; _warpToCurrentIndex(); } } void _warpToCurrentIndex() { if (!mounted || _pageController!.page == _currentIndex!.toDouble()) { return; } final adjacentDestination = (_currentIndex! - _controller!.previousIndex).abs() == 1; if (adjacentDestination) { _warpToAdjacentTab(_controller!.animationDuration); } else { _warpToNonAdjacentTab(_controller!.animationDuration); } } Future _warpToAdjacentTab(Duration duration) async { if (duration == Duration.zero) { _jumpToPage(_currentIndex!); } else { await _animateToPage( _currentIndex!, duration: duration, curve: Curves.ease, ); } if (mounted) { setState(_updateChildren); } return Future.value(); } Future _warpToNonAdjacentTab(Duration duration) async { final int previousIndex = _controller!.previousIndex; assert((_currentIndex! - previousIndex).abs() > 1); // initialPage defines which page is shown when starting the animation. // This page is adjacent to the destination page. final int initialPage = _currentIndex! > previousIndex ? _currentIndex! - 1 : _currentIndex! + 1; setState(() { // Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children. // For motivation, see https://github.com/flutter/flutter/pull/29188 and // https://github.com/flutter/flutter/issues/27010#issuecomment-486475152. _childrenWithKey = List.of(_childrenWithKey, growable: false); final Widget temp = _childrenWithKey[initialPage]; _childrenWithKey[initialPage] = _childrenWithKey[previousIndex]; _childrenWithKey[previousIndex] = temp; }); // Make a first jump to the adjacent page. _jumpToPage(initialPage); // Jump or animate to the destination page. if (duration == Duration.zero) { _jumpToPage(_currentIndex!); } else { await _animateToPage( _currentIndex!, duration: duration, curve: Curves.ease, ); } if (mounted) { setState(_updateChildren); } } void _syncControllerOffset() { _controller!.offset = clampDouble( _pageController!.page! - _controller!.index, -1.0, 1.0, ); } // Called when the PageView scrolls bool _handleScrollNotification(ScrollNotification notification) { if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) { return false; } if (notification.depth != 0) { return false; } if (!_controllerIsValid) { return false; } _scrollUnderwayCount += 1; final double page = _pageController!.page!; if (notification is ScrollUpdateNotification && !_controller!.indexIsChanging) { final bool pageChanged = (page - _controller!.index).abs() > 1.0; if (pageChanged) { _controller!.index = page.round(); _currentIndex = _controller!.index; } _syncControllerOffset(); } else if (notification is ScrollEndNotification) { _controller!.index = page.round(); _currentIndex = _controller!.index; if (!_controller!.indexIsChanging) { _syncControllerOffset(); } } _scrollUnderwayCount -= 1; return false; } bool _debugScheduleCheckHasValidChildrenCount() { if (_debugHasScheduledValidChildrenCountCheck) { return true; } WidgetsBinding.instance.addPostFrameCallback((Duration duration) { _debugHasScheduledValidChildrenCountCheck = false; if (!mounted) { return; } assert(() { if (_controller!.length != widget.children.length) { throw FlutterError( "Controller's length property (${_controller!.length}) does not match the " "number of children (${widget.children.length}) present in TabBarView's children property.", ); } return true; }()); }, debugLabel: 'TabBarView.validChildrenCountCheck'); _debugHasScheduledValidChildrenCountCheck = true; return true; } @override Widget build(BuildContext context) { assert(_debugScheduleCheckHasValidChildrenCount()); return NotificationListener( onNotification: _handleScrollNotification, child: PageView( dragStartBehavior: widget.dragStartBehavior, clipBehavior: widget.clipBehavior, controller: _pageController, physics: widget.physics == null ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) : const PageScrollPhysics().applyTo(widget.physics), horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, children: _childrenWithKey, ), ); } } ================================================ FILE: lib/common/widgets/flutter/pop_scope.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart' hide PopScope; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; abstract class PopScopeState extends State implements PopEntry { ModalRoute? _route; @override void onPopInvoked(bool didPop) {} @override late final ValueNotifier canPopNotifier; bool get initCanPop => true; @override void initState() { super.initState(); canPopNotifier = ValueNotifier(initCanPop); _route = (Get.routing.route as ModalRoute)..registerPopEntry(this); } @override void dispose() { _route?.unregisterPopEntry(this); _route = null; canPopNotifier.dispose(); super.dispose(); } } // ignore: camel_case_types typedef popScope = PopScope; class PopScope extends StatefulWidget { const PopScope({ super.key, required this.child, this.canPop = true, required this.onPopInvokedWithResult, }); final Widget child; final PopInvokedWithResultCallback onPopInvokedWithResult; final bool canPop; @override State createState() => _PopScopeState(); } class _PopScopeState extends PopScopeState { @override bool get initCanPop => widget.canPop; @override void onPopInvokedWithResult(bool didPop, Object? result) { widget.onPopInvokedWithResult(didPop, result); } @override void didUpdateWidget(T oldWidget) { super.didUpdateWidget(oldWidget); canPopNotifier.value = widget.canPop; } @override Widget build(BuildContext context) => widget.child; } ================================================ FILE: lib/common/widgets/flutter/popup_menu.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library; import 'package:flutter/material.dart'; class CustomPopupMenuItem extends PopupMenuEntry { const CustomPopupMenuItem({ super.key, this.value, this.height = kMinInteractiveDimension, required this.child, }); final T? value; @override final double height; final Widget? child; @override bool represents(T? value) => value == this.value; @override CustomPopupMenuItemState> createState() => CustomPopupMenuItemState>(); } class CustomPopupMenuItemState> extends State { @protected @override Widget build(BuildContext context) { final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); const Set states = {}; final style = popupMenuTheme.labelTextStyle?.resolve(states)! ?? _PopupMenuDefaultsM3(context).labelTextStyle!.resolve(states)!; return ListTileTheme.merge( contentPadding: .zero, titleTextStyle: style, child: AnimatedDefaultTextStyle( style: style, duration: kThemeChangeDuration, child: ConstrainedBox( constraints: BoxConstraints(minHeight: widget.height), child: Padding( padding: _PopupMenuDefaultsM3.menuItemPadding, child: Align( alignment: AlignmentDirectional.centerStart, child: widget.child, ), ), ), ), ); } } class CustomPopupMenuDivider extends PopupMenuEntry { const CustomPopupMenuDivider({ super.key, required this.height, this.thickness, this.indent, this.endIndent, this.radius, }); @override final double height; final double? thickness; final double? indent; final double? endIndent; final BorderRadiusGeometry? radius; @override bool represents(void value) => false; @override State createState() => _CustomPopupMenuDividerState(); } class _CustomPopupMenuDividerState extends State { @override Widget build(BuildContext context) { return Divider( height: widget.height, thickness: widget.thickness, indent: widget.indent, color: ColorScheme.of(context).outline.withValues(alpha: 0.2), endIndent: widget.endIndent, radius: widget.radius, ); } } // BEGIN GENERATED TOKEN PROPERTIES - PopupMenu // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // dart format off class _PopupMenuDefaultsM3 extends PopupMenuThemeData { _PopupMenuDefaultsM3(this.context) : super(elevation: 3.0); final BuildContext context; late final ThemeData _theme = Theme.of(context); late final ColorScheme _colors = _theme.colorScheme; late final TextTheme _textTheme = _theme.textTheme; @override WidgetStateProperty? get labelTextStyle { return WidgetStateProperty.resolveWith((Set states) { // TODO(quncheng): Update this hard-coded value to use the latest tokens. final TextStyle style = _textTheme.labelLarge!; if (states.contains(WidgetState.disabled)) { return style.apply(color: _colors.onSurface.withValues(alpha: 0.38)); } return style.apply(color: _colors.onSurface); }); } @override Color? get color => _colors.surfaceContainer; @override Color? get shadowColor => _colors.shadow; @override Color? get surfaceTintColor => Colors.transparent; @override ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); // TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs // Update this when the token is available. @override EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0); // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs // Update this when the token is available. static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0); }// dart format on // END GENERATED TOKEN PROPERTIES - PopupMenu ================================================ FILE: lib/common/widgets/flutter/refresh_indicator.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async' show Completer; import 'dart:io' show Platform; import 'package:PiliPlus/common/widgets/scroll_behavior.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' show RefreshScrollPhysics; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/material.dart' hide RefreshIndicator; /// The distance from the child's top or bottom [edgeOffset] where /// the refresh indicator will settle. During the drag that exposes the refresh /// indicator, its actual displacement may significantly exceed this value. /// /// In most cases, [displacement] distance starts counting from the parent's /// edges. However, if [edgeOffset] is larger than zero then the [displacement] /// value is calculated from that offset instead of the parent's edge. double displacement = Pref.refreshDisplacement; // The over-scroll distance that moves the indicator to its maximum // displacement, as a percentage of the scrollable's container extent. double kDragContainerExtentPercentage = Pref.refreshDragPercentage; // How much the scroll's drag gesture can overshoot the RefreshIndicator's // displacement; max displacement = _kDragSizeFactorLimit * displacement. const double _kDragSizeFactorLimit = 1.5; // When the scroll ends, the duration of the refresh indicator's animation // to the RefreshIndicator's displacement. const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150); // The duration of the ScaleTransition that starts when the refresh action // has completed. const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200); /// Indicates current status of Material `RefreshIndicator`. enum RefreshIndicatorStatus { /// Pointer is down. drag, /// Dragged far enough that an up event will run the onRefresh callback. // armed, /// Animating to the indicator's final "displacement". snap, /// Running the refresh callback. refresh, /// Animating the indicator's fade-out after refreshing. done, /// Animating the indicator's fade-out after not arming. canceled, } /// A widget that supports the Material "swipe to refresh" idiom. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM} /// /// When the child's [Scrollable] descendant overscrolls, an animated circular /// progress indicator is faded into view. When the scroll ends, if the /// indicator has been dragged far enough for it to become completely opaque, /// the [onRefresh] callback is called. The callback is expected to update the /// scrollable's contents and then complete the [Future] it returns. The refresh /// indicator disappears after the callback's [Future] has completed. /// /// The trigger mode is configured by [RefreshIndicator.triggerMode]. /// /// {@tool dartpad} /// This example shows how [RefreshIndicator] can be triggered in different ways. /// /// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This example shows how to trigger [RefreshIndicator] in a nested scroll view using /// the [notificationPredicate] property. /// /// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This example shows how to use [RefreshIndicator] without the spinner. /// /// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart ** /// {@end-tool} /// /// ## Troubleshooting /// /// ### Refresh indicator does not show up /// /// The [RefreshIndicator] will appear if its scrollable descendant can be /// overscrolled, i.e. if the scrollable's content is bigger than its viewport. /// To ensure that the [RefreshIndicator] will always appear, even if the /// scrollable's content fits within its viewport, set the scrollable's /// [Scrollable.physics] property to [AlwaysScrollableScrollPhysics]: /// /// ```dart /// ListView( /// physics: const AlwaysScrollableScrollPhysics(), /// // ... /// ) /// ``` /// /// A [RefreshIndicator] can only be used with a vertical scroll view. /// /// See also: /// /// * /// * [RefreshIndicatorState], can be used to programmatically show the refresh indicator. /// * [RefreshProgressIndicator], widget used by [RefreshIndicator] to show /// the inner circular progress spinner during refreshes. /// * [CupertinoSliverRefreshControl], an iOS equivalent of the pull-to-refresh pattern. /// Must be used as a sliver inside a [CustomScrollView] instead of wrapping /// around a [ScrollView] because it's a part of the scrollable instead of /// being overlaid on top of it. class RefreshIndicator extends StatefulWidget { /// Creates a refresh indicator. /// /// The [onRefresh], [child], and [notificationPredicate] arguments must be /// non-null. The default /// [displacement] is 40.0 logical pixels. /// /// The [semanticsLabel] is used to specify an accessibility label for this widget. /// If it is null, it will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]. /// An empty string may be passed to avoid having anything read by screen reading software. /// The [semanticsValue] may be used to specify progress on the widget. const RefreshIndicator({ super.key, this.edgeOffset = 0.0, required this.onRefresh, this.color, this.backgroundColor, this.notificationPredicate = defaultScrollNotificationPredicate, this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth, this.elevation = 2.0, this.isClampingScrollPhysics = false, required this.child, }) : assert(elevation >= 0.0); /// The widget below this widget in the tree. /// /// The refresh indicator will be stacked on top of this child. The indicator /// will appear when child's Scrollable descendant is over-scrolled. /// /// Typically a [ListView] or [CustomScrollView]. final Widget child; /// The offset where [RefreshProgressIndicator] starts to appear on drag start. /// /// Depending whether the indicator is showing on the top or bottom, the value /// of this variable controls how far from the parent's edge the progress /// indicator starts to appear. This may come in handy when, for example, the /// UI contains a top [Widget] which covers the parent's edge where the progress /// indicator would otherwise appear. /// /// By default, the edge offset is set to 0. /// /// See also: /// /// * [displacement], can be used to change the distance from the edge that /// the indicator settles. final double edgeOffset; /// A function that's called when the user has dragged the refresh indicator /// far enough to demonstrate that they want the app to refresh. The returned /// [Future] must complete when the refresh operation is finished. final RefreshCallback onRefresh; /// The progress indicator's foreground color. The current theme's /// [ColorScheme.primary] by default. final Color? color; /// The progress indicator's background color. The current theme's /// [ThemeData.canvasColor] by default. final Color? backgroundColor; /// A check that specifies whether a [ScrollNotification] should be /// handled by this widget. /// /// By default, checks whether `notification.depth == 0`. Set it to something /// else for more complicated layouts. final ScrollNotificationPredicate notificationPredicate; /// Defines [strokeWidth] for `RefreshIndicator`. /// /// By default, the value of [strokeWidth] is 2.0 pixels. final double strokeWidth; /// Defines the elevation of the underlying [RefreshIndicator]. /// /// Defaults to 2.0. final double elevation; final bool isClampingScrollPhysics; @override RefreshIndicatorState createState() => RefreshIndicatorState(); } /// Contains the state for a [RefreshIndicator]. This class can be used to /// programmatically show the refresh indicator, see the [show] method. class RefreshIndicatorState extends State with TickerProviderStateMixin { late AnimationController _positionController; late AnimationController _scaleController; late Animation _positionFactor; late Animation _scaleFactor; late Animation _value; late Animation _valueColor; RefreshIndicatorStatus? _status; late Future _pendingRefreshFuture; double? _dragOffset; late Color _effectiveValueColor = widget.color ?? Theme.of(context).colorScheme.primary; static final Animatable _threeQuarterTween = Tween( begin: 0.0, end: 0.75, ); static final Animatable _kDragSizeFactorLimitTween = Tween( begin: 0.0, end: _kDragSizeFactorLimit, ); static final Animatable _oneToZeroTween = Tween( begin: 1.0, end: 0.0, ); @protected @override void initState() { super.initState(); _positionController = AnimationController(vsync: this); _positionFactor = _positionController.drive(_kDragSizeFactorLimitTween); // The "value" of the circular progress indicator during a drag. _value = _positionController.drive(_threeQuarterTween); _scaleController = AnimationController(vsync: this); _scaleFactor = _scaleController.drive(_oneToZeroTween); } @protected @override void didChangeDependencies() { _setupColorTween(); super.didChangeDependencies(); } @protected @override void didUpdateWidget(covariant RefreshIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.color != widget.color) { _setupColorTween(); } } @protected @override void dispose() { _positionController.dispose(); _scaleController.dispose(); super.dispose(); } void _setupColorTween() { // Reset the current value color. _effectiveValueColor = widget.color ?? Theme.of(context).colorScheme.primary; final Color color = _effectiveValueColor; if (color.a == 0) { // Set an always stopped animation instead of a driven tween. _valueColor = AlwaysStoppedAnimation(color); } else { // Respect the alpha of the given color. _valueColor = _positionController.drive( ColorTween( begin: color.withValues(alpha: 0), end: color, ).chain( CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)), ), ); } } bool _shouldStart(ScrollNotification notification) { // If the notification.dragDetails is null, this scroll is not triggered by // user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll. // In this case, we don't want to trigger the refresh indicator. return _status == null && ((notification is ScrollStartNotification && notification.dragDetails != null) || (notification is ScrollUpdateNotification && notification.dragDetails != null)) && notification.metrics.extentBefore == 0.0 && _start(); } bool _handleScrollNotification(ScrollNotification notification) { if (!widget.notificationPredicate(notification)) { return false; } if (_shouldStart(notification)) { setState(() { _status = RefreshIndicatorStatus.drag; }); return false; } if (notification is ScrollUpdateNotification) { if (_status == RefreshIndicatorStatus.drag) { _dragOffset = _dragOffset! - notification.scrollDelta!; _checkDragOffset(notification.metrics.viewportDimension); if (notification.dragDetails == null && _valueColor.value!.a == _effectiveValueColor.a) { // On iOS start the refresh when the Scrollable bounces back from the // overscroll (ScrollNotification indicating this don't have dragDetails // because the scroll activity is not directly triggered by a drag). _show(); } } } else if (notification is OverscrollNotification) { if (_status == RefreshIndicatorStatus.drag) { _dragOffset = _dragOffset! - notification.overscroll; _checkDragOffset(notification.metrics.viewportDimension); } } else if (notification is ScrollEndNotification) { switch (_status) { case RefreshIndicatorStatus.drag: if (_valueColor.value!.a == _effectiveValueColor.a) { _show(); } else { _dismiss(RefreshIndicatorStatus.canceled); } case RefreshIndicatorStatus.canceled: case RefreshIndicatorStatus.done: case RefreshIndicatorStatus.refresh: case RefreshIndicatorStatus.snap: case null: // do nothing break; } } return false; } bool _handleIndicatorNotification( OverscrollIndicatorNotification notification, ) { if (notification.depth != 0 || !notification.leading) { return false; } if (_status == RefreshIndicatorStatus.drag) { notification.disallowIndicator(); return true; } return false; } bool _start() { assert(_status == null); assert(_dragOffset == null); _dragOffset = 0.0; _scaleController.value = 0.0; _positionController.value = 0.0; return true; } void _checkDragOffset(double containerExtent) { assert( _status == RefreshIndicatorStatus.drag, ); double newValue = _dragOffset! / (containerExtent * kDragContainerExtentPercentage); _positionController.value = clampDouble( newValue, 0.0, 1.0, ); // This triggers various rebuilds. } // Stop showing the refresh indicator. Future _dismiss(RefreshIndicatorStatus newMode) async { await Future.value(); // This can only be called from _show() when refreshing and // _handleScrollNotification in response to a ScrollEndNotification or // direction change. assert( newMode == RefreshIndicatorStatus.canceled || newMode == RefreshIndicatorStatus.done, ); setState(() { _status = newMode; }); switch (_status!) { case RefreshIndicatorStatus.done: await _scaleController.animateTo( 1.0, duration: _kIndicatorScaleDuration, ); case RefreshIndicatorStatus.canceled: await _positionController.animateTo( 0.0, duration: _kIndicatorScaleDuration, ); case RefreshIndicatorStatus.drag: case RefreshIndicatorStatus.refresh: case RefreshIndicatorStatus.snap: assert(false); } if (mounted && _status == newMode) { _dragOffset = null; setState(() { _status = null; }); } } void _show() { assert(_status != RefreshIndicatorStatus.refresh); assert(_status != RefreshIndicatorStatus.snap); final Completer completer = Completer(); _pendingRefreshFuture = completer.future; _status = RefreshIndicatorStatus.snap; _positionController .animateTo( 1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration, ) .whenComplete(() { if (mounted && _status == RefreshIndicatorStatus.snap) { setState(() { // Show the indeterminate progress indicator. _status = RefreshIndicatorStatus.refresh; }); widget.onRefresh().whenComplete(() { if (mounted && _status == RefreshIndicatorStatus.refresh) { completer.complete(); _dismiss(RefreshIndicatorStatus.done); } }); } }); } /// Show the refresh indicator and run the refresh callback as if it had /// been started interactively. If this method is called while the refresh /// callback is running, it quietly does nothing. /// /// Creating the [RefreshIndicator] with a [GlobalKey] /// makes it possible to refer to the [RefreshIndicatorState]. /// /// The future returned from this method completes when the /// [RefreshIndicator.onRefresh] callback's future completes. /// /// If you await the future returned by this function from a [State], you /// should check that the state is still [mounted] before calling [setState]. /// /// When initiated in this manner, the refresh indicator is independent of any /// actual scroll view. It defaults to showing the indicator at the top. To /// show it at the bottom, set `atTop` to false. Future show() { if (_status != RefreshIndicatorStatus.refresh && _status != RefreshIndicatorStatus.snap) { if (_status == null) { _start(); } _show(); } return _pendingRefreshFuture; } @protected @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); Widget child = NotificationListener( onNotification: _handleScrollNotification, child: NotificationListener( onNotification: _handleIndicatorNotification, child: widget.child, ), ); assert(() { if (_status == null) { assert(_dragOffset == null); } else { assert(_dragOffset != null); } return true; }()); final bool showIndeterminateIndicator = _status == RefreshIndicatorStatus.refresh || _status == RefreshIndicatorStatus.done; child = Stack( clipBehavior: Clip.none, children: [ child, if (_status != null) Positioned( top: widget.edgeOffset, left: 0.0, right: 0.0, child: SizeTransition( axisAlignment: 1.0, sizeFactor: _positionFactor, // This is what brings it down. child: Padding( padding: EdgeInsets.only(top: displacement), child: Align( alignment: Alignment.topCenter, child: ScaleTransition( scale: _scaleFactor, child: AnimatedBuilder( animation: _positionController, builder: (context, child) => RefreshProgressIndicator( value: showIndeterminateIndicator ? null : _value.value, valueColor: _valueColor, backgroundColor: widget.backgroundColor, strokeWidth: widget.strokeWidth, elevation: widget.elevation, ), ), ), ), ), ), ), ], ); if (!widget.isClampingScrollPhysics && (Platform.isIOS || Platform.isMacOS)) { return child; } return ScrollConfiguration( behavior: RefreshScrollBehavior( desktopDragDevices, scrollPhysics: RefreshScrollPhysics( parent: const RangeMaintainingScrollPhysics(), onDrag: _onDrag, ), ), child: child, ); } bool _onDrag(double offset, double viewportDimension) { if (_positionController.value > 0.0 && _status == RefreshIndicatorStatus.drag) { _dragOffset = _dragOffset! + offset; _checkDragOffset(viewportDimension); return true; } return false; } } // ignore: camel_case_types typedef refreshIndicator = RefreshIndicator; class RefreshScrollBehavior extends CustomScrollBehavior { const RefreshScrollBehavior( super.dragDevices, { required this.scrollPhysics, }); final RefreshScrollPhysics scrollPhysics; @override ScrollPhysics getScrollPhysics(BuildContext context) { return scrollPhysics; } } ================================================ FILE: lib/common/widgets/flutter/selectable_text/selectable_region.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/tap_and_drag.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' hide BaseTapAndDragGestureRecognizer, TapAndHorizontalDragGestureRecognizer, TapAndPanGestureRecognizer; import 'package:flutter/material.dart' hide SelectableRegion; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; // Examples can assume: // late GlobalKey key; const Set _kLongPressSelectionDevices = { PointerDeviceKind.touch, PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, }; /// A widget that introduces an area for user selections. /// /// Flutter widgets are not selectable by default. Wrapping a widget subtree /// with a [SelectableRegion] widget enables selection within that subtree (for /// example, [Text] widgets automatically look for selectable regions to enable /// selection). The wrapped subtree can be selected by users using mouse or /// touch gestures, e.g. users can select widgets by holding the mouse /// left-click and dragging across widgets, or they can use long press gestures /// to select words on touch devices. /// /// A [SelectableRegion] widget requires configuration; in particular specific /// [selectionControls] must be provided. /// /// The [SelectionArea] widget from the [material] library configures a /// [SelectableRegion] in a platform-specific manner (e.g. using a Material /// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be /// simpler to use that widget rather than using [SelectableRegion] directly. /// /// ## An overview of the selection system. /// /// Every [Selectable] under the [SelectableRegion] can be selected. They form a /// selection tree structure to handle the selection. /// /// The [SelectableRegion] is a wrapper over [SelectionContainer]. It listens to /// user gestures and sends corresponding [SelectionEvent]s to the /// [SelectionContainer] it creates. /// /// A [SelectionContainer] is a single [Selectable] that handles /// [SelectionEvent]s on behalf of child [Selectable]s in the subtree. It /// creates a [SelectionRegistrarScope] with its [SelectionContainer.delegate] /// to collect child [Selectable]s and sends the [SelectionEvent]s it receives /// from the parent [SelectionRegistrar] to the appropriate child [Selectable]s. /// It creates an abstraction for the parent [SelectionRegistrar] as if it is /// interacting with a single [Selectable]. /// /// The [SelectionContainer] created by [SelectableRegion] is the root node of a /// selection tree. Each non-leaf node in the tree is a [SelectionContainer], /// and the leaf node is a leaf widget whose render object implements /// [Selectable]. They are connected through [SelectionRegistrarScope]s created /// by [SelectionContainer]s. /// /// Both [SelectionContainer]s and the leaf [Selectable]s need to register /// themselves to the [SelectionRegistrar] from the /// [SelectionContainer.maybeOf] if they want to participate in the /// selection. /// /// An example selection tree will look like: /// /// {@tool snippet} /// /// ```dart /// MaterialApp( /// home: SelectableRegion( /// selectionControls: materialTextSelectionControls, /// child: Scaffold( /// appBar: AppBar(title: const Text('Flutter Code Sample')), /// body: ListView( /// children: const [ /// Text('Item 0', style: TextStyle(fontSize: 50.0)), /// Text('Item 1', style: TextStyle(fontSize: 50.0)), /// ], /// ), /// ), /// ), /// ) /// ``` /// {@end-tool} /// /// /// SelectionContainer /// (SelectableRegion) /// / \ /// / \ /// / \ /// Selectable \ /// ("Flutter Code Sample") \ /// \ /// SelectionContainer /// (ListView) /// / \ /// / \ /// / \ /// Selectable Selectable /// ("Item 0") ("Item 1") /// /// /// ## Making a widget selectable /// /// Some leaf widgets, such as [Text], have all of the selection logic wired up /// automatically and can be selected as long as they are under a /// [SelectableRegion]. /// /// To make a custom selectable widget, its render object needs to mix in /// [Selectable] and implement the required APIs to handle [SelectionEvent]s /// as well as paint appropriate selection highlights. /// /// The render object also needs to register itself to a [SelectionRegistrar]. /// For the most cases, one can use [SelectionRegistrant] to auto-register /// itself with the register returned from [SelectionContainer.maybeOf] as /// seen in the example below. /// /// {@tool dartpad} /// This sample demonstrates how to create an adapter widget that makes any /// child widget selectable. /// /// ** See code in examples/api/lib/material/selectable_region/selectable_region.0.dart ** /// {@end-tool} /// /// ## Complex layout /// /// By default, the screen order is used as the selection order. If a group of /// [Selectable]s needs to select differently, consider wrapping them with a /// [SelectionContainer] to customize its selection behavior. /// /// {@tool dartpad} /// This sample demonstrates how to create a [SelectionContainer] that only /// allows selecting everything or nothing with no partial selection. /// /// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart ** /// {@end-tool} /// /// In the case where a group of widgets should be excluded from selection under /// a [SelectableRegion], consider wrapping that group of widgets using /// [SelectionContainer.disabled]. /// /// {@tool dartpad} /// This sample demonstrates how to disable selection for a Text in a Column. /// /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** /// {@end-tool} /// /// To create a separate selection system from its parent selection area, /// wrap part of the subtree with another [SelectableRegion]. The selection of the /// child selection area can not extend past its subtree, and the selection of /// the parent selection area can not extend inside the child selection area. /// /// ## Selection status /// /// A [SelectableRegion]s [SelectableRegionSelectionStatus] is used to indicate whether /// the [SelectableRegion] is actively changing the selection, or has finalized it. For /// example, during a mouse click + drag, the [SelectableRegionSelectionStatus] will be /// set to [SelectableRegionSelectionStatus.changing], and when the mouse click is released /// the status will be set to [SelectableRegionSelectionStatus.finalized]. /// /// The default value of [SelectableRegion]s selection status /// is [SelectableRegionSelectionStatus.finalized]. /// /// To access the [SelectableRegionSelectionStatus] of a parent [SelectableRegion] /// use [SelectableRegionSelectionStatusScope.maybeOf] and retrieve the value from /// the [ValueListenable]. /// /// One can also listen for changes to the [SelectableRegionSelectionStatus] by /// adding a listener to the [ValueListenable] retrieved from [SelectableRegionSelectionStatusScope.maybeOf] /// through [ValueListenable.addListener]. In Stateful widgets this is typically /// done in [State.didChangeDependencies]. Remove the listener when no longer /// needed, typically in your Stateful widgets [State.dispose] method through /// [ValueListenable.removeListener]. /// /// ## Tests /// /// In a test, a region can be selected either by faking drag events (e.g. using /// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region /// that has been given a [GlobalKey], e.g.: /// /// ```dart /// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard)); /// ``` /// /// See also: /// /// * [SelectionArea], which creates a [SelectableRegion] with /// platform-adaptive selection controls. /// * [SelectableText], which enables selection on a single run of text. /// * [SelectionHandler], which contains APIs to handle selection events from the /// [SelectableRegion]. /// * [Selectable], which provides API to participate in the selection system. /// * [SelectionRegistrar], which [Selectable] needs to subscribe to receive /// selection events. /// * [SelectionContainer], which collects selectable widgets in the subtree /// and provides api to dispatch selection event to the collected widget. /// * [SelectionListener], which enables accessing the [SelectionDetails] of /// the selectable subtree it wraps. class SelectableRegion extends StatefulWidget { /// Create a new [SelectableRegion] widget. /// /// The [selectionControls] are used for building the selection handles and /// toolbar for mobile devices. const SelectableRegion({ super.key, this.contextMenuBuilder, this.focusNode, this.magnifierConfiguration = TextMagnifierConfiguration.disabled, this.onSelectionChanged, required this.selectionControls, required this.child, }); /// The configuration for the magnifier used with selections in this region. /// /// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled. /// For a version of [SelectableRegion] that adapts automatically to the /// current platform, consider [SelectionArea]. /// /// {@macro flutter.widgets.magnifier.intro} final TextMagnifierConfiguration magnifierConfiguration; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// The child widget this selection area applies to. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} final SelectableRegionContextMenuBuilder? contextMenuBuilder; /// The delegate to build the selection handles and toolbar for mobile /// devices. /// /// The [emptyTextSelectionControls] global variable provides a default /// [TextSelectionControls] implementation with no controls. final TextSelectionControls selectionControls; /// Called when the selected content changes. final ValueChanged? onSelectionChanged; /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu. /// /// For example, [SelectableRegion] uses this to generate the default buttons /// for its context menu. /// /// See also: /// /// * [SelectableRegionState.contextMenuButtonItems], which gives the /// [ContextMenuButtonItem]s for a specific SelectableRegion. /// * [EditableText.getEditableButtonItems], which performs a similar role but /// for content that is both selectable and editable. /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can /// take a list of [ContextMenuButtonItem]s with /// [AdaptiveTextSelectionToolbar.buttonItems]. /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button /// Widgets for the current platform given [ContextMenuButtonItem]s. static List getSelectableButtonItems({ required final SelectionGeometry selectionGeometry, required final VoidCallback onCopy, required final VoidCallback onSelectAll, required final VoidCallback? onShare, }) { final canCopy = selectionGeometry.status == SelectionStatus.uncollapsed; final bool canSelectAll = selectionGeometry.hasContent; // The share button is not supported on the web. final bool platformCanShare = !kIsWeb && switch (defaultTargetPlatform) { TargetPlatform.android => selectionGeometry.status == SelectionStatus.uncollapsed, TargetPlatform.macOS || TargetPlatform.fuchsia || TargetPlatform.linux || TargetPlatform.windows => false, // TODO(bleroux): the share button should be shown on iOS but the share // functionality requires some changes on the engine side because, on iPad, // it needs an anchor for the popup. // See: https://github.com/flutter/flutter/issues/141775. TargetPlatform.iOS => false, }; final bool canShare = onShare != null && platformCanShare; // On Android, the share button is before the select all button. final showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android; // Determine which buttons will appear so that the order and total number is // known. A button's position in the menu can slightly affect its // appearance. return [ if (canCopy) ContextMenuButtonItem( onPressed: onCopy, type: ContextMenuButtonType.copy, ), if (canShare && showShareBeforeSelectAll) ContextMenuButtonItem( onPressed: onShare, type: ContextMenuButtonType.share, ), if (canSelectAll) ContextMenuButtonItem( onPressed: onSelectAll, type: ContextMenuButtonType.selectAll, ), if (canShare && !showShareBeforeSelectAll) ContextMenuButtonItem( onPressed: onShare, type: ContextMenuButtonType.share, ), ]; } @override State createState() => SelectableRegionState(); } /// State for a [SelectableRegion]. class SelectableRegionState extends State with TextSelectionDelegate implements SelectionRegistrar { late final Map> _actions = >{ SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( _GranularlyExtendSelectionAction< ExtendSelectionToNextWordBoundaryOrCaretLocationIntent >( this, granularity: TextGranularity.word, ), ), ExpandSelectionToDocumentBoundaryIntent: _makeOverridable( _GranularlyExtendSelectionAction( this, granularity: TextGranularity.document, ), ), ExpandSelectionToLineBreakIntent: _makeOverridable( _GranularlyExtendSelectionAction( this, granularity: TextGranularity.line, ), ), ExtendSelectionByCharacterIntent: _makeOverridable( _GranularlyExtendCaretSelectionAction( this, granularity: TextGranularity.character, ), ), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( _GranularlyExtendCaretSelectionAction< ExtendSelectionToNextWordBoundaryIntent >( this, granularity: TextGranularity.word, ), ), ExtendSelectionToLineBreakIntent: _makeOverridable( _GranularlyExtendCaretSelectionAction( this, granularity: TextGranularity.line, ), ), ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable( _DirectionallyExtendCaretSelectionAction< ExtendSelectionVerticallyToAdjacentLineIntent >(this), ), ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( _GranularlyExtendCaretSelectionAction< ExtendSelectionToDocumentBoundaryIntent >( this, granularity: TextGranularity.document, ), ), }; final Map _gestureRecognizers = {}; SelectionOverlay? _selectionOverlay; final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _endHandleLayerLink = LayerLink(); final LayerLink _toolbarLayerLink = LayerLink(); final StaticSelectionContainerDelegate _selectionDelegate = StaticSelectionContainerDelegate(); // there should only ever be one selectable, which is the SelectionContainer. Selectable? _selectable; bool get _hasSelectionOverlayGeometry => _selectionDelegate.value.startSelectionPoint != null || _selectionDelegate.value.endSelectionPoint != null; Orientation? _lastOrientation; SelectedContent? _lastSelectedContent; /// Whether the native browser context menu is enabled. // TODO(Renzo-Olivares): Re-enable web context menu for Android // and iOS when https://github.com/flutter/flutter/issues/177123 // is resolved. bool get _webContextMenuEnabled => kIsWeb && BrowserContextMenu.enabled && defaultTargetPlatform != TargetPlatform.android && defaultTargetPlatform != TargetPlatform.iOS; /// The [SelectionOverlay] that is currently visible on the screen. /// /// Can be null if there is no visible [SelectionOverlay]. @visibleForTesting SelectionOverlay? get selectionOverlay => _selectionOverlay; /// The text processing service used to retrieve the native text processing actions. final ProcessTextService _processTextService = DefaultProcessTextService(); /// The list of native text processing actions provided by the engine. final List _processTextActions = []; // The focus node to use if the widget didn't supply one. FocusNode? _localFocusNode; FocusNode get _focusNode => widget.focusNode ?? (_localFocusNode ??= FocusNode(debugLabel: 'SelectableRegion')); /// Notifies its listeners when the selection state in this [SelectableRegion] changes. final _SelectableRegionSelectionStatusNotifier _selectionStatusNotifier = _SelectableRegionSelectionStatusNotifier._(); @protected @override void initState() { super.initState(); _focusNode.addListener(_handleFocusChanged); _initMouseGestureRecognizer(); _initTouchGestureRecognizer(); // Right clicks. _gestureRecognizers[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { instance.onSecondaryTapDown = _handleRightClickDown; }, ); _initProcessTextActions(); } /// Query the engine to initialize the list of text processing actions to show /// in the text selection toolbar. Future _initProcessTextActions() async { _processTextActions ..clear() ..addAll(await _processTextService.queryTextActions()); } @protected @override void didChangeDependencies() { super.didChangeDependencies(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: break; case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: return; } // Hide the text selection toolbar on mobile when orientation changes. final Orientation orientation = MediaQuery.orientationOf(context); if (_lastOrientation == null) { _lastOrientation = orientation; return; } if (orientation != _lastOrientation) { _lastOrientation = orientation; hideToolbar(defaultTargetPlatform == TargetPlatform.android); } } @protected @override void didUpdateWidget(SelectableRegion oldWidget) { super.didUpdateWidget(oldWidget); if (widget.focusNode != oldWidget.focusNode) { if (oldWidget.focusNode == null && widget.focusNode != null) { _localFocusNode?.removeListener(_handleFocusChanged); _localFocusNode?.dispose(); _localFocusNode = null; } else if (widget.focusNode == null && oldWidget.focusNode != null) { oldWidget.focusNode!.removeListener(_handleFocusChanged); } _focusNode.addListener(_handleFocusChanged); if (_focusNode.hasFocus != oldWidget.focusNode?.hasFocus) { _handleFocusChanged(); } } } Action _makeOverridable(Action defaultAction) { return Action.overridable( context: context, defaultAction: defaultAction, ); } void _handleFocusChanged() { if (!_focusNode.hasFocus) { if (_webContextMenuEnabled) { PlatformSelectableRegionContextMenu.detach(_selectionDelegate); } if (SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) { // We should only clear the selection when this SelectableRegion loses // focus while the application is currently running. It is possible // that the application is not currently running, for example on desktop // platforms, clicking on a different window switches the focus to // the new window causing the Flutter application to go inactive. In this // case we want to retain the selection so it remains when we return to // the Flutter application. clearSelection(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); } } if (_webContextMenuEnabled) { PlatformSelectableRegionContextMenu.attach(_selectionDelegate); } } void _updateSelectionStatus() { final SelectionGeometry geometry = _selectionDelegate.value; final TextSelection selection = switch (geometry.status) { SelectionStatus.uncollapsed || SelectionStatus.collapsed => const TextSelection(baseOffset: 0, extentOffset: 1), SelectionStatus.none => const TextSelection.collapsed(offset: 1), }; textEditingValue = TextEditingValue(text: '__', selection: selection); if (_hasSelectionOverlayGeometry) { _updateSelectionOverlay(); } else { _selectionOverlay?.dispose(); _selectionOverlay = null; } } // gestures. /// Whether the Shift key was pressed when the most recent [PointerDownEvent] /// was tracked by the [BaseTapAndDragGestureRecognizer]. bool _isShiftPressed = false; // The position of the most recent secondary tap down event on this // SelectableRegion. Offset? _lastSecondaryTapDownPosition; // The device kind for the pointer of the most recent tap down event on this // SelectableRegion. PointerDeviceKind? _lastPointerDeviceKind; static bool _isPrecisePointerDevice(PointerDeviceKind pointerDeviceKind) { switch (pointerDeviceKind) { case PointerDeviceKind.mouse: return true; case PointerDeviceKind.trackpad: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: return false; } } void _finalizeSelectableRegionStatus() { if (_selectionStatusNotifier.value != SelectableRegionSelectionStatus.changing) { // Don't finalize the selection again if it is not currently changing. return; } _selectionStatusNotifier.value = SelectableRegionSelectionStatus.finalized; } // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, // which can grow to be infinitely large, to a value between 1 and the supported // max consecutive tap count. The value that the raw count is converted to is // based on the default observed behavior on the native platforms. // // This method should be used in all instances when details.consecutiveTapCount // would be used. int _getEffectiveConsecutiveTapCount(int rawCount) { var maxConsecutiveTap = 3; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: if (_lastPointerDeviceKind != null && _lastPointerDeviceKind != PointerDeviceKind.mouse) { // When the pointer device kind is not precise like a mouse, native // Android resets the tap count at 2. For example, this is so the // selection can collapse on the third tap. maxConsecutiveTap = 2; } // From observation, these platforms reset their tap count to 0 when // the number of consecutive taps exceeds the max consecutive tap supported. // For example on native Android, when going past a triple click, // on the fourth click the selection is moved to the precise click // position, on the fifth click the word at the position is selected, and // on the sixth click the paragraph at the position is selected. return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap); case TargetPlatform.linux: // From observation, these platforms reset their tap count to 0 when // the number of consecutive taps exceeds the max consecutive tap supported. // For example on Debian Linux with GTK, when going past a triple click, // on the fourth click the selection is moved to the precise click // position, on the fifth click the word at the position is selected, and // on the sixth click the paragraph at the position is selected. return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap); case TargetPlatform.iOS: case TargetPlatform.macOS: case TargetPlatform.windows: // From observation, these platforms hold their tap count at the max // consecutive tap supported. For example on macOS, when going past a triple // click, the selection should be retained at the paragraph that was first // selected on triple click. return min(rawCount, maxConsecutiveTap); } } void _initMouseGestureRecognizer() { _gestureRecognizers[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapAndPanGestureRecognizer( debugOwner: this, supportedDevices: {PointerDeviceKind.mouse}, ), (TapAndPanGestureRecognizer instance) { instance ..onTapTrackStart = _onTapTrackStart ..onTapTrackReset = _onTapTrackReset ..onTapDown = _startNewMouseSelectionGesture ..onTapUp = _handleMouseTapUp ..onDragStart = _handleMouseDragStart ..onDragUpdate = _handleMouseDragUpdate ..onDragEnd = _handleMouseDragEnd ..onCancel = clearSelection ..dragStartBehavior = DragStartBehavior.down; }, ); } void _onTapTrackStart() { _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed.intersection( { LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight, }, ).isNotEmpty; } void _onTapTrackReset() { _isShiftPressed = false; } void _initTouchGestureRecognizer() { // A [TapAndHorizontalDragGestureRecognizer] is used on non-precise pointer devices // like PointerDeviceKind.touch so [SelectableRegion] gestures do not conflict with // ancestor Scrollable gestures in common scenarios like a vertically scrolling list view. _gestureRecognizers[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers< TapAndHorizontalDragGestureRecognizer >( () => TapAndHorizontalDragGestureRecognizer( debugOwner: this, supportedDevices: PointerDeviceKind.values.where(( PointerDeviceKind device, ) { return device != PointerDeviceKind.mouse; }).toSet(), ), (TapAndHorizontalDragGestureRecognizer instance) { instance // iOS does not provide a device specific touch slop // unlike Android (~8.0), so the touch slop for a [Scrollable] // always default to kTouchSlop which is 18.0. When // [SelectableRegion] is the child of a horizontal // scrollable that means the [SelectableRegion] will // always win the gesture arena when competing with // the ancestor scrollable because they both have // the same touch slop threshold and the child receives // the [PointerEvent] first. To avoid this conflict // and ensure a smooth scrolling experience, on // iOS the [TapAndHorizontalDragGestureRecognizer] // will wait for all other gestures to lose before // declaring victory. ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS ..onTapDown = _startNewMouseSelectionGesture ..onTapUp = _handleMouseTapUp ..onDragStart = _handleMouseDragStart ..onDragUpdate = _handleMouseDragUpdate ..onDragEnd = _handleMouseDragEnd ..onCancel = clearSelection ..dragStartBehavior = DragStartBehavior.down; }, ); _gestureRecognizers[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( debugOwner: this, supportedDevices: _kLongPressSelectionDevices, ), (LongPressGestureRecognizer instance) { instance ..onLongPressStart = _handleTouchLongPressStart ..onLongPressMoveUpdate = _handleTouchLongPressMoveUpdate ..onLongPressEnd = _handleTouchLongPressEnd; }, ); } Offset? _doubleTapOffset; void _startNewMouseSelectionGesture(TapDragDownDetails details) { _lastPointerDeviceKind = details.kind; switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { case 1: _focusNode.requestFocus(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: // On mobile platforms the selection is set on tap up for the first // tap. break; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: hideToolbar(); // It is impossible to extend the selection when the shift key is // pressed and the start of the selection has not been initialized. // In this case we fallback on collapsing the selection to first // initialize the selection. final bool isShiftPressedValid = _isShiftPressed && _selectionDelegate.value.startSelectionPoint != null; if (isShiftPressedValid) { _selectEndTo(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; break; } clearSelection(); _collapseSelectionAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } case 2: switch (defaultTargetPlatform) { case TargetPlatform.iOS: if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!)) { // Double tap on iOS web triggers when a drag begins after the double tap. _doubleTapOffset = details.globalPosition; break; } _selectWordAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) { _showHandles(); } case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: _selectWordAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } case 3: switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: if (details.kind != null && _isPrecisePointerDevice(details.kind!)) { // Triple tap on static text is only supported on mobile // platforms using a precise pointer device. _selectParagraphAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: _selectParagraphAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } } _updateSelectedContentIfNeeded(); } void _handleMouseDragStart(TapDragStartDetails details) { switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { case 1: if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) { // Drag to select is only enabled with a precise pointer device. return; } _selectStartTo(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } _updateSelectedContentIfNeeded(); } void _handleMouseDragUpdate(TapDragUpdateDetails details) { switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { case 1: if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) { // Drag to select is only enabled with a precise pointer device. return; } _selectEndTo(offset: details.globalPosition, continuous: true); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; case 2: switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: // Double tap + drag is only supported on Android when using a precise // pointer device or when not on the web. if (!kIsWeb || details.kind != null && _isPrecisePointerDevice(details.kind!)) { _selectEndTo( offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word, ); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } case TargetPlatform.iOS: if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!) && _doubleTapOffset != null) { // On iOS web a double tap does not select the word at the position, // until the drag has begun. _selectWordAt(offset: _doubleTapOffset!); _doubleTapOffset = null; } _selectEndTo( offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word, ); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) { _showHandles(); } case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: _selectEndTo( offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word, ); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } case 3: switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: // Triple tap + drag is only supported on mobile devices when using // a precise pointer device. if (details.kind != null && _isPrecisePointerDevice(details.kind!)) { _selectEndTo( offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph, ); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: _selectEndTo( offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph, ); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } } _updateSelectedContentIfNeeded(); } void _handleMouseDragEnd(TapDragEndDetails details) { assert(_lastPointerDeviceKind != null); final bool isPointerPrecise = _isPrecisePointerDevice( _lastPointerDeviceKind!, ); // On mobile platforms like android, fuchsia, and iOS, a drag gesture will // only show the selection overlay when the drag has finished and the pointer // device kind is not precise, for example at the end of a double tap + drag // to select on native iOS. final bool shouldShowSelectionOverlayOnMobile = !isPointerPrecise; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: if (shouldShowSelectionOverlayOnMobile) { _showHandles(); _showToolbar(); } case TargetPlatform.iOS: if (shouldShowSelectionOverlayOnMobile) { _showToolbar(); } case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: // The selection overlay is not shown on desktop platforms after a drag. break; } _finalizeSelection(); _updateSelectedContentIfNeeded(); _finalizeSelectableRegionStatus(); } void _handleMouseTapUp(TapDragUpDetails details) { if (defaultTargetPlatform == TargetPlatform.iOS && _positionIsOnActiveSelection(globalPosition: details.globalPosition)) { // On iOS when the tap occurs on the previous selection, instead of // moving the selection, the context menu will be toggled. final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false; if (toolbarIsVisible) { hideToolbar(false); } else { _showToolbar(); } return; } switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { case 1: switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: hideToolbar(); _collapseSelectionAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: // On desktop platforms the selection is set on tap down. } case 2: final bool isPointerPrecise = _isPrecisePointerDevice(details.kind); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: if (!isPointerPrecise) { // On Android, a double tap will only show the selection overlay after // the following tap up when the pointer device kind is not precise. _showHandles(); _showToolbar(); } case TargetPlatform.iOS: if (!isPointerPrecise) { if (kIsWeb) { // Double tap on iOS web only triggers when a drag begins after the double tap. break; } // On iOS, a double tap will only show the selection toolbar after // the following tap up when the pointer device kind is not precise. _showToolbar(); } case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: // The selection overlay is not shown on desktop platforms // on a double click. break; } } _finalizeSelectableRegionStatus(); _updateSelectedContentIfNeeded(); } void _updateSelectedContentIfNeeded() { if (widget.onSelectionChanged == null) { return; } final SelectedContent? content = _selectable?.getSelectedContent(); if (_lastSelectedContent?.plainText != content?.plainText) { _lastSelectedContent = content; widget.onSelectionChanged!.call(_lastSelectedContent); } } void _handleTouchLongPressStart(LongPressStartDetails details) { HapticFeedback.selectionClick(); _focusNode.requestFocus(); _selectWordAt(offset: details.globalPosition); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; // Platforms besides Android will show the text selection handles when // the long press is initiated. Android shows the text selection handles when // the long press has ended, usually after a pointer up event is received. if (defaultTargetPlatform != TargetPlatform.android) { _showHandles(); } _updateSelectedContentIfNeeded(); } void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) { _selectEndTo( offset: details.globalPosition, textGranularity: TextGranularity.word, ); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _updateSelectedContentIfNeeded(); } void _handleTouchLongPressEnd(LongPressEndDetails details) { _finalizeSelection(); _updateSelectedContentIfNeeded(); _finalizeSelectableRegionStatus(); _showToolbar(); if (defaultTargetPlatform == TargetPlatform.android) { _showHandles(); } } bool _positionIsOnActiveSelection({required Offset globalPosition}) { for (final Rect selectionRect in _selectionDelegate.value.selectionRects) { final Matrix4 transform = _selectable!.getTransformTo(null); final Rect globalRect = MatrixUtils.transformRect( transform, selectionRect, ); if (globalRect.contains(globalPosition)) { return true; } } return false; } void _handleRightClickDown(TapDownDetails details) { final Offset? previousSecondaryTapDownPosition = _lastSecondaryTapDownPosition; final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false; _lastSecondaryTapDownPosition = details.globalPosition; _focusNode.requestFocus(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.windows: // If _lastSecondaryTapDownPosition is within the current selection then // keep the current selection, if not then collapse it. final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection( globalPosition: details.globalPosition, ); if (lastSecondaryTapDownPositionWasOnActiveSelection) { // Restore _lastSecondaryTapDownPosition since it may be cleared if a user // accesses contextMenuAnchors. _lastSecondaryTapDownPosition = details.globalPosition; _showHandles(); _showToolbar(location: _lastSecondaryTapDownPosition); _updateSelectedContentIfNeeded(); return; } _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!); case TargetPlatform.iOS: _selectWordAt(offset: _lastSecondaryTapDownPosition!); case TargetPlatform.macOS: if (previousSecondaryTapDownPosition == _lastSecondaryTapDownPosition && toolbarIsVisible) { hideToolbar(); return; } _selectWordAt(offset: _lastSecondaryTapDownPosition!); case TargetPlatform.linux: if (toolbarIsVisible) { hideToolbar(); return; } // If _lastSecondaryTapDownPosition is within the current selection then // keep the current selection, if not then collapse it. final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection( globalPosition: details.globalPosition, ); if (!lastSecondaryTapDownPositionWasOnActiveSelection) { _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!); } } _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); // Restore _lastSecondaryTapDownPosition since it may be cleared if a user // accesses contextMenuAnchors. _lastSecondaryTapDownPosition = details.globalPosition; _showHandles(); _showToolbar(location: _lastSecondaryTapDownPosition); _updateSelectedContentIfNeeded(); } // Selection update helper methods. Offset? _selectionEndPosition; bool get _userDraggingSelectionEnd => _selectionEndPosition != null; bool _scheduledSelectionEndEdgeUpdate = false; /// Sends end [SelectionEdgeUpdateEvent] to the selectable subtree. /// /// If the selectable subtree returns a [SelectionResult.pending], this method /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result /// is not pending or users end their gestures. void _triggerSelectionEndEdgeUpdate({TextGranularity? textGranularity}) { // This method can be called when the drag is not in progress. This can // happen if the child scrollable returns SelectionResult.pending, and // the selection area scheduled a selection update for the next frame, but // the drag is lifted before the scheduled selection update is run. if (_scheduledSelectionEndEdgeUpdate || !_userDraggingSelectionEnd) { return; } if (_selectable?.dispatchSelectionEvent( SelectionEdgeUpdateEvent.forEnd( globalPosition: _selectionEndPosition!, granularity: textGranularity, ), ) == SelectionResult.pending) { _scheduledSelectionEndEdgeUpdate = true; SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { if (!_scheduledSelectionEndEdgeUpdate) { return; } _scheduledSelectionEndEdgeUpdate = false; _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); }, debugLabel: 'SelectableRegion.endEdgeUpdate'); return; } } void _onAnyDragEnd(DragEndDetails details) { final bool draggingHandles = _selectionOverlay != null && (_selectionOverlay!.isDraggingStartHandle || _selectionOverlay!.isDraggingEndHandle); if (!draggingHandles) { _selectionOverlay!.hideMagnifier(); _showToolbar(); } _finalizeSelection(); _updateSelectedContentIfNeeded(); _finalizeSelectableRegionStatus(); } void _stopSelectionEndEdgeUpdate() { _scheduledSelectionEndEdgeUpdate = false; _selectionEndPosition = null; } Offset? _selectionStartPosition; bool get _userDraggingSelectionStart => _selectionStartPosition != null; bool _scheduledSelectionStartEdgeUpdate = false; /// Sends start [SelectionEdgeUpdateEvent] to the selectable subtree. /// /// If the selectable subtree returns a [SelectionResult.pending], this method /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result /// is not pending or users end their gestures. void _triggerSelectionStartEdgeUpdate({TextGranularity? textGranularity}) { // This method can be called when the drag is not in progress. This can // happen if the child scrollable returns SelectionResult.pending, and // the selection area scheduled a selection update for the next frame, but // the drag is lifted before the scheduled selection update is run. if (_scheduledSelectionStartEdgeUpdate || !_userDraggingSelectionStart) { return; } if (_selectable?.dispatchSelectionEvent( SelectionEdgeUpdateEvent.forStart( globalPosition: _selectionStartPosition!, granularity: textGranularity, ), ) == SelectionResult.pending) { _scheduledSelectionStartEdgeUpdate = true; SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { if (!_scheduledSelectionStartEdgeUpdate) { return; } _scheduledSelectionStartEdgeUpdate = false; _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); }, debugLabel: 'SelectableRegion.startEdgeUpdate'); return; } } void _stopSelectionStartEdgeUpdate() { _scheduledSelectionStartEdgeUpdate = false; _selectionEndPosition = null; } // SelectionOverlay helper methods. late Offset _selectionStartHandleDragPosition; late Offset _selectionEndHandleDragPosition; void _handleSelectionStartHandleDragStart(DragStartDetails details) { assert(_selectionDelegate.value.startSelectionPoint != null); final Offset localPosition = _selectionDelegate.value.startSelectionPoint!.localPosition; final Matrix4 globalTransform = _selectable!.getTransformTo(null); _selectionStartHandleDragPosition = MatrixUtils.transformPoint( globalTransform, localPosition, ); _selectionOverlay!.showMagnifier( _buildInfoForMagnifier( details.globalPosition, _selectionDelegate.value.startSelectionPoint!, ), ); _updateSelectedContentIfNeeded(); } void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) { _selectionStartHandleDragPosition = _selectionStartHandleDragPosition + details.delta; // The value corresponds to the paint origin of the selection handle. // Offset it to the center of the line to make it feel more natural. _selectionStartPosition = _selectionStartHandleDragPosition - Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2); _triggerSelectionStartEdgeUpdate(); _selectionOverlay!.updateMagnifier( _buildInfoForMagnifier( details.globalPosition, _selectionDelegate.value.startSelectionPoint!, ), ); _updateSelectedContentIfNeeded(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } void _handleSelectionEndHandleDragStart(DragStartDetails details) { assert(_selectionDelegate.value.endSelectionPoint != null); final Offset localPosition = _selectionDelegate.value.endSelectionPoint!.localPosition; final Matrix4 globalTransform = _selectable!.getTransformTo(null); _selectionEndHandleDragPosition = MatrixUtils.transformPoint( globalTransform, localPosition, ); _selectionOverlay!.showMagnifier( _buildInfoForMagnifier( details.globalPosition, _selectionDelegate.value.endSelectionPoint!, ), ); _updateSelectedContentIfNeeded(); } void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { _selectionEndHandleDragPosition = _selectionEndHandleDragPosition + details.delta; // The value corresponds to the paint origin of the selection handle. // Offset it to the center of the line to make it feel more natural. _selectionEndPosition = _selectionEndHandleDragPosition - Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2); _triggerSelectionEndEdgeUpdate(); _selectionOverlay!.updateMagnifier( _buildInfoForMagnifier( details.globalPosition, _selectionDelegate.value.endSelectionPoint!, ), ); _updateSelectedContentIfNeeded(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; } MagnifierInfo _buildInfoForMagnifier( Offset globalGesturePosition, SelectionPoint selectionPoint, ) { final Vector3 globalTransform = _selectable! .getTransformTo(null) .getTranslation(); final globalTransformAsOffset = Offset( globalTransform.x, globalTransform.y, ); final Offset globalSelectionPointPosition = selectionPoint.localPosition + globalTransformAsOffset; final caretRect = Rect.fromLTWH( globalSelectionPointPosition.dx, globalSelectionPointPosition.dy - selectionPoint.lineHeight, 0, selectionPoint.lineHeight, ); return MagnifierInfo( globalGesturePosition: globalGesturePosition, caretRect: caretRect, fieldBounds: globalTransformAsOffset & _selectable!.size, currentLineBoundaries: globalTransformAsOffset & _selectable!.size, ); } void _createSelectionOverlay() { assert(_hasSelectionOverlayGeometry); if (_selectionOverlay != null) { return; } final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; _selectionOverlay = SelectionOverlay( context: context, debugRequiredFor: widget, startHandleType: start?.handleType ?? TextSelectionHandleType.collapsed, lineHeightAtStart: start?.lineHeight ?? end!.lineHeight, onStartHandleDragStart: _handleSelectionStartHandleDragStart, onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate, onStartHandleDragEnd: _onAnyDragEnd, endHandleType: end?.handleType ?? TextSelectionHandleType.collapsed, lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight, onEndHandleDragStart: _handleSelectionEndHandleDragStart, onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate, onEndHandleDragEnd: _onAnyDragEnd, selectionEndpoints: selectionEndpoints, selectionControls: widget.selectionControls, selectionDelegate: this, clipboardStatus: null, startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, toolbarLayerLink: _toolbarLayerLink, magnifierConfiguration: widget.magnifierConfiguration, ); } void _updateSelectionOverlay() { if (_selectionOverlay == null) { return; } assert(_hasSelectionOverlayGeometry); final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; _selectionOverlay! ..startHandleType = start?.handleType ?? TextSelectionHandleType.left ..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight ..endHandleType = end?.handleType ?? TextSelectionHandleType.right ..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight ..selectionEndpoints = selectionEndpoints; } /// Shows the selection handles. /// /// Returns true if the handles are shown, false if the handles can't be /// shown. bool _showHandles() { if (_selectionOverlay != null) { _selectionOverlay!.showHandles(); return true; } if (!_hasSelectionOverlayGeometry) { return false; } _createSelectionOverlay(); _selectionOverlay!.showHandles(); return true; } /// Shows the text selection toolbar. /// /// If the parameter `location` is set, the toolbar will be shown at the /// location. Otherwise, the toolbar location will be calculated based on the /// handles' locations. The `location` is in the coordinates system of the /// [Overlay]. /// /// Returns true if the toolbar is shown, false if the toolbar can't be shown. bool _showToolbar({Offset? location}) { if (!_hasSelectionOverlayGeometry && _selectionOverlay == null) { return false; } // Web is using native dom elements to enable clipboard functionality of the // context menu: copy, paste, select, cut. It might also provide additional // functionality depending on the browser (such as translate). Due to this, // we should not show a Flutter toolbar for the editable text elements // unless the browser's context menu is explicitly disabled. if (_webContextMenuEnabled) { return false; } if (_selectionOverlay == null) { _createSelectionOverlay(); } _selectionOverlay!.toolbarLocation = location; // TODO(Renzo-Olivares): Remove the logic below that does a runtimeType // check for TextSelectionHandleControls when TextSelectionHandleControls // is fully removed, see: https://github.com/flutter/flutter/pull/124262. if (widget.selectionControls is! TextSelectionHandleControls) { _selectionOverlay!.showToolbar(); return true; } _selectionOverlay!.hideToolbar(); _selectionOverlay!.showToolbar( context: context, contextMenuBuilder: (BuildContext context) { return widget.contextMenuBuilder!(context, this); }, ); return true; } /// Sets or updates selection end edge to the `offset` location. /// /// A selection always contains a select start edge and selection end edge. /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or /// use other selection APIs, such as [_selectWordAt] or [selectAll]. /// /// This method sets or updates the selection end edge by sending /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s. /// /// If `continuous` is set to true and the update causes scrolling, the /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the /// child [Selectable]s every frame until the scrolling finishes or a /// [_finalizeSelection] is called. /// /// The `continuous` argument defaults to false. /// /// The `offset` is in global coordinates. /// /// Provide the `textGranularity` if the selection should not move by the default /// [TextGranularity.character]. Only [TextGranularity.character] and /// [TextGranularity.word] are currently supported. /// /// See also: /// * [_selectStartTo], which sets or updates selection start edge. /// * [_finalizeSelection], which stops the `continuous` updates. /// * [clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [selectAll], which selects the entire content. void _selectEndTo({ required Offset offset, bool continuous = false, TextGranularity? textGranularity, }) { if (!continuous) { _selectable?.dispatchSelectionEvent( SelectionEdgeUpdateEvent.forEnd( globalPosition: offset, granularity: textGranularity, ), ); return; } if (_selectionEndPosition != offset) { _selectionEndPosition = offset; _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); } } /// Sets or updates selection start edge to the `offset` location. /// /// A selection always contains a select start edge and selection end edge. /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or /// use other selection APIs, such as [_selectWordAt] or [selectAll]. /// /// This method sets or updates the selection start edge by sending /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s. /// /// If `continuous` is set to true and the update causes scrolling, the /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the /// child [Selectable]s every frame until the scrolling finishes or a /// [_finalizeSelection] is called. /// /// The `continuous` argument defaults to false. /// /// The `offset` is in global coordinates. /// /// Provide the `textGranularity` if the selection should not move by the default /// [TextGranularity.character]. Only [TextGranularity.character] and /// [TextGranularity.word] are currently supported. /// /// See also: /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. /// * [clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [selectAll], which selects the entire content. void _selectStartTo({ required Offset offset, bool continuous = false, TextGranularity? textGranularity, }) { if (!continuous) { _selectable?.dispatchSelectionEvent( SelectionEdgeUpdateEvent.forStart( globalPosition: offset, granularity: textGranularity, ), ); return; } if (_selectionStartPosition != offset) { _selectionStartPosition = offset; _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); } } /// Collapses the selection at the given `offset` location. /// /// The `offset` is in global coordinates. /// /// See also: /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. /// * [clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [selectAll], which selects the entire content. void _collapseSelectionAt({required Offset offset}) { // There may be other selection ongoing. _finalizeSelection(); _selectStartTo(offset: offset); _selectEndTo(offset: offset); } /// Selects a whole word at the `offset` location. /// /// The `offset` is in global coordinates. /// /// If the whole word is already in the current selection, selection won't /// change. One call [clearSelection] first if the selection needs to be /// updated even if the word is already covered by the current selection. /// /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection /// edges after calling this method. /// /// See also: /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. /// * [clearSelection], which clears the ongoing selection. /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [selectAll], which selects the entire content. void _selectWordAt({required Offset offset}) { // There may be other selection ongoing. _finalizeSelection(); _selectable?.dispatchSelectionEvent( SelectWordSelectionEvent(globalPosition: offset), ); } /// Selects the entire paragraph at the `offset` location. /// /// The `offset` is in global coordinates. /// /// If the paragraph is already in the current selection, selection won't /// change. One call [clearSelection] first if the selection needs to be /// updated even if the paragraph is already covered by the current selection. /// /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection /// edges after calling this method. /// /// See also: /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. /// * [clearSelection], which clear the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [selectAll], which selects the entire content. void _selectParagraphAt({required Offset offset}) { // There may be other selection ongoing. _finalizeSelection(); _selectable?.dispatchSelectionEvent( SelectParagraphSelectionEvent(globalPosition: offset), ); } /// Stops any ongoing selection updates. /// /// This method is different from [clearSelection] that it does not remove /// the current selection. It only stops the continuous updates. /// /// A continuous update can happen as result of calling [_selectStartTo] or /// [_selectEndTo] with `continuous` sets to true which causes a [Selectable] /// to scroll. Calling this method will stop the update as well as the /// scrolling. void _finalizeSelection() { _stopSelectionEndEdgeUpdate(); _stopSelectionStartEdgeUpdate(); } /// Removes the ongoing selection for this [SelectableRegion]. void clearSelection() { _finalizeSelection(); _directionalHorizontalBaseline = null; _adjustingSelectionEnd = null; _selectable?.dispatchSelectionEvent(const ClearSelectionEvent()); _updateSelectedContentIfNeeded(); } Future _copy() async { final SelectedContent? data = _selectable?.getSelectedContent(); if (data == null) { return; } await Clipboard.setData(ClipboardData(text: data.plainText)); } Future _share() async { final SelectedContent? data = _selectable?.getSelectedContent(); if (data == null) { return; } await SystemChannels.platform.invokeMethod('Share.invoke', data.plainText); } /// {@macro flutter.widgets.EditableText.getAnchors} /// /// See also: /// /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s /// for the default context menu buttons. TextSelectionToolbarAnchors get contextMenuAnchors { if (_lastSecondaryTapDownPosition != null) { final anchors = TextSelectionToolbarAnchors( primaryAnchor: _lastSecondaryTapDownPosition!, ); // Clear the state of _lastSecondaryTapDownPosition after use since a user may // access contextMenuAnchors and receive invalid anchors for their context menu. _lastSecondaryTapDownPosition = null; return anchors; } final renderBox = context.findRenderObject()! as RenderBox; return TextSelectionToolbarAnchors.fromSelection( renderBox: renderBox, startGlyphHeight: startGlyphHeight, endGlyphHeight: endGlyphHeight, selectionEndpoints: selectionEndpoints, ); } bool? _adjustingSelectionEnd; bool _determineIsAdjustingSelectionEnd(bool forward) { if (_adjustingSelectionEnd != null) { return _adjustingSelectionEnd!; } final bool isReversed; final SelectionPoint start = _selectionDelegate.value.startSelectionPoint!; final SelectionPoint end = _selectionDelegate.value.endSelectionPoint!; if (start.localPosition.dy > end.localPosition.dy) { isReversed = true; } else if (start.localPosition.dy < end.localPosition.dy) { isReversed = false; } else { isReversed = start.localPosition.dx > end.localPosition.dx; } // Always move the selection edge that increases the selection range. return _adjustingSelectionEnd = forward != isReversed; } void _granularlyExtendSelection(TextGranularity granularity, bool forward) { _directionalHorizontalBaseline = null; if (!_selectionDelegate.value.hasSelection) { return; } _selectable?.dispatchSelectionEvent( GranularlyExtendSelectionEvent( forward: forward, isEnd: _determineIsAdjustingSelectionEnd(forward), granularity: granularity, ), ); _updateSelectedContentIfNeeded(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); } double? _directionalHorizontalBaseline; void _directionallyExtendSelection(bool forward) { if (!_selectionDelegate.value.hasSelection) { return; } final bool adjustingSelectionExtend = _determineIsAdjustingSelectionEnd( forward, ); final SelectionPoint baseLinePoint = adjustingSelectionExtend ? _selectionDelegate.value.endSelectionPoint! : _selectionDelegate.value.startSelectionPoint!; _directionalHorizontalBaseline ??= baseLinePoint.localPosition.dx; final Offset globalSelectionPointOffset = MatrixUtils.transformPoint( context.findRenderObject()!.getTransformTo(null), Offset(_directionalHorizontalBaseline!, 0), ); _selectable?.dispatchSelectionEvent( DirectionallyExtendSelectionEvent( isEnd: _adjustingSelectionEnd!, direction: forward ? SelectionExtendDirection.nextLine : SelectionExtendDirection.previousLine, dx: globalSelectionPointOffset.dx, ), ); _updateSelectedContentIfNeeded(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); } // [TextSelectionDelegate] overrides. /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu. /// /// See also: /// /// * [SelectableRegion.getSelectableButtonItems], which performs a similar role, /// but for any selectable text, not just specifically SelectableRegion. /// * [EditableTextState.contextMenuButtonItems], which performs a similar role /// but for content that is not just selectable but also editable. /// * [contextMenuAnchors], which provides the anchor points for the default /// context menu. /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can /// take a list of [ContextMenuButtonItem]s with /// [AdaptiveTextSelectionToolbar.buttonItems]. /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the /// button Widgets for the current platform given [ContextMenuButtonItem]s. List get contextMenuButtonItems { return SelectableRegion.getSelectableButtonItems( selectionGeometry: _selectionDelegate.value, onCopy: () { _copy(); // On Android copy should clear the selection. switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: clearSelection(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); case TargetPlatform.iOS: hideToolbar(false); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: hideToolbar(); } }, onSelectAll: () { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: case TargetPlatform.fuchsia: selectAll(SelectionChangedCause.toolbar); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: selectAll(); hideToolbar(); } }, onShare: () { _share(); // On Android, share should clear the selection. switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: clearSelection(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); case TargetPlatform.iOS: hideToolbar(false); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: hideToolbar(); } }, )..addAll(_textProcessingActionButtonItems); } List get _textProcessingActionButtonItems { final buttonItems = []; final SelectedContent? data = _selectable?.getSelectedContent(); if (data == null) { return buttonItems; } for (final ProcessTextAction action in _processTextActions) { buttonItems.add( ContextMenuButtonItem( label: action.label, onPressed: () async { final String selectedText = data.plainText; if (selectedText.isNotEmpty) { await _processTextService.processTextAction( action.id, selectedText, true, ); hideToolbar(); } }, ), ); } return buttonItems; } /// The line height at the start of the current selection. double get startGlyphHeight { return _selectionDelegate.value.startSelectionPoint!.lineHeight; } /// The line height at the end of the current selection. double get endGlyphHeight { return _selectionDelegate.value.endSelectionPoint!.lineHeight; } /// Returns the local coordinates of the endpoints of the current selection. List get selectionEndpoints { final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; late List points; final Offset startLocalPosition = start?.localPosition ?? end!.localPosition; final Offset endLocalPosition = end?.localPosition ?? start!.localPosition; if (startLocalPosition.dy > endLocalPosition.dy) { points = [ TextSelectionPoint(endLocalPosition, TextDirection.ltr), TextSelectionPoint(startLocalPosition, TextDirection.ltr), ]; } else { points = [ TextSelectionPoint(startLocalPosition, TextDirection.ltr), TextSelectionPoint(endLocalPosition, TextDirection.ltr), ]; } return points; } // [TextSelectionDelegate] overrides. // TODO(justinmc): After deprecations have been removed, remove // TextSelectionDelegate from this class. // https://github.com/flutter/flutter/issues/111213 @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override bool get cutEnabled => false; @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override bool get pasteEnabled => false; @override void hideToolbar([bool hideHandles = true]) { _selectionOverlay?.hideToolbar(); if (hideHandles) { _selectionOverlay?.hideHandles(); } } @override void selectAll([SelectionChangedCause? cause]) { clearSelection(); _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent()); if (cause == SelectionChangedCause.toolbar) { _showToolbar(); _showHandles(); } _updateSelectedContentIfNeeded(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); } @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override void copySelection(SelectionChangedCause cause) { _copy(); clearSelection(); _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; _finalizeSelectableRegionStatus(); } @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override TextEditingValue textEditingValue = const TextEditingValue(text: '_'); @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override void bringIntoView(TextPosition position) { /* SelectableRegion must be in view at this point. */ } @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override void cutSelection(SelectionChangedCause cause) { assert(false); } @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override void userUpdateTextEditingValue( TextEditingValue value, SelectionChangedCause cause, ) { /* SelectableRegion maintains its own state */ } @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override Future pasteText(SelectionChangedCause cause) async { assert(false); } // [SelectionRegistrar] override. @override void add(Selectable selectable) { assert(_selectable == null); _selectable = selectable; _selectable!.addListener(_updateSelectionStatus); _selectable!.pushHandleLayers(_startHandleLayerLink, _endHandleLayerLink); } @override void remove(Selectable selectable) { assert(_selectable == selectable); _selectable!.removeListener(_updateSelectionStatus); _selectable!.pushHandleLayers(null, null); _selectable = null; } @protected @override void dispose() { _selectable?.removeListener(_updateSelectionStatus); _selectable?.pushHandleLayers(null, null); _selectionDelegate.dispose(); _selectionStatusNotifier.dispose(); // In case dispose was triggered before gesture end, remove the magnifier // so it doesn't remain stuck in the overlay forever. _selectionOverlay?.hideMagnifier(); _selectionOverlay?.dispose(); _selectionOverlay = null; widget.focusNode?.removeListener(_handleFocusChanged); _localFocusNode?.removeListener(_handleFocusChanged); _localFocusNode?.dispose(); super.dispose(); } @protected @override Widget build(BuildContext context) { assert(debugCheckHasOverlay(context)); Widget result = SelectableRegionSelectionStatusScope._( selectionStatusNotifier: _selectionStatusNotifier, child: SelectionContainer( registrar: this, delegate: _selectionDelegate, child: widget.child, ), ); if (_webContextMenuEnabled) { result = PlatformSelectableRegionContextMenu(child: result); } return CompositedTransformTarget( link: _toolbarLayerLink, child: RawGestureDetector( gestures: _gestureRecognizers, behavior: HitTestBehavior.translucent, excludeFromSemantics: true, child: Actions( actions: _actions, child: Focus.withExternalFocusNode( includeSemantics: false, focusNode: _focusNode, child: result, ), ), ), ); } } /// An action that does not override any [Action.overridable] in the subtree. /// /// If this action is invoked by an [Action.overridable], it will immediately /// invoke the [Action.overridable] and do nothing else. Otherwise, it will call /// [invokeAction]. abstract class _NonOverrideAction extends ContextAction { Object? invokeAction(T intent, [BuildContext? context]); @override Object? invoke(T intent, [BuildContext? context]) { if (callingAction != null) { return callingAction!.invoke(intent); } return invokeAction(intent, context); } } class _SelectAllAction extends _NonOverrideAction { _SelectAllAction(this.state); final SelectableRegionState state; @override void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) { state.selectAll(SelectionChangedCause.keyboard); } } class _CopySelectionAction extends _NonOverrideAction { _CopySelectionAction(this.state); final SelectableRegionState state; @override void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) { state._copy(); } } class _GranularlyExtendSelectionAction extends _NonOverrideAction { _GranularlyExtendSelectionAction(this.state, {required this.granularity}); final SelectableRegionState state; final TextGranularity granularity; @override void invokeAction(T intent, [BuildContext? context]) { state._granularlyExtendSelection(granularity, intent.forward); } } class _GranularlyExtendCaretSelectionAction< T extends DirectionalCaretMovementIntent > extends _NonOverrideAction { _GranularlyExtendCaretSelectionAction( this.state, { required this.granularity, }); final SelectableRegionState state; final TextGranularity granularity; @override void invokeAction(T intent, [BuildContext? context]) { if (intent.collapseSelection) { // Selectable region never collapses selection. return; } state._granularlyExtendSelection(granularity, intent.forward); } } class _DirectionallyExtendCaretSelectionAction< T extends DirectionalCaretMovementIntent > extends _NonOverrideAction { _DirectionallyExtendCaretSelectionAction(this.state); final SelectableRegionState state; @override void invokeAction(T intent, [BuildContext? context]) { if (intent.collapseSelection) { // Selectable region never collapses selection. return; } state._directionallyExtendSelection(intent.forward); } } /// Signature for a widget builder that builds a context menu for the given /// [SelectableRegionState]. /// /// See also: /// /// * [EditableTextContextMenuBuilder], which performs the same role for /// [EditableText]. typedef SelectableRegionContextMenuBuilder = Widget Function( BuildContext context, SelectableRegionState selectableRegionState, ); /// Notifies its listeners when the [SelectableRegion] that created this object /// is changing or finalizes its selection. /// /// To access the [_SelectableRegionSelectionStatusNotifier] from the nearest [SelectableRegion] /// ancestor, use [SelectableRegionSelectionStatusScope.maybeOf]. final class _SelectableRegionSelectionStatusNotifier extends ChangeNotifier implements ValueListenable { _SelectableRegionSelectionStatusNotifier._(); SelectableRegionSelectionStatus _selectableRegionSelectionStatus = SelectableRegionSelectionStatus.finalized; /// The current value of the [SelectableRegionSelectionStatus] of the [SelectableRegion] /// that owns this object. /// /// Defaults to [SelectableRegionSelectionStatus.finalized]. @override SelectableRegionSelectionStatus get value => _selectableRegionSelectionStatus; /// Sets the [SelectableRegionSelectionStatus] for the [SelectableRegion] that /// owns this object. /// /// Listeners are notified even if the value did not change. @protected set value(SelectableRegionSelectionStatus newStatus) { assert( newStatus == SelectableRegionSelectionStatus.finalized && value == SelectableRegionSelectionStatus.changing || newStatus == SelectableRegionSelectionStatus.changing, 'Attempting to finalize the selection when it is already finalized.', ); _selectableRegionSelectionStatus = newStatus; notifyListeners(); } } /// Notifies its listeners when the selection under a [SelectableRegion] or /// [SelectionArea] is being changed or finalized. /// /// Use [SelectableRegionSelectionStatusScope.maybeOf], to access the [ValueListenable] of type /// [SelectableRegionSelectionStatus] under a [SelectableRegion]. Its listeners /// will be called even when the value of the [SelectableRegionSelectionStatus] /// does not change. final class SelectableRegionSelectionStatusScope extends InheritedWidget { const SelectableRegionSelectionStatusScope._({ required this.selectionStatusNotifier, required super.child, }); /// Tracks updates to the [SelectableRegionSelectionStatus] of the owning /// [SelectableRegion]. /// /// Listeners will be called even when the value of the [SelectableRegionSelectionStatus] /// does not change. The selection under the [SelectableRegion] still may have changed. final ValueListenable selectionStatusNotifier; /// The closest instance of this class that encloses the given context. /// /// If there is no enclosing [SelectableRegion] or [SelectionArea] widget, then null is /// returned. /// /// Calling this method will create a dependency on the closest /// [SelectableRegionSelectionStatusScope] in the [context], if there is one. static ValueListenable? maybeOf( BuildContext context, ) { return context .dependOnInheritedWidgetOfExactType< SelectableRegionSelectionStatusScope >() ?.selectionStatusNotifier; } @override bool updateShouldNotify(SelectableRegionSelectionStatusScope oldWidget) { return selectionStatusNotifier != oldWidget.selectionStatusNotifier; } } ================================================ FILE: lib/common/widgets/flutter/selectable_text/selectable_text.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:PiliPlus/common/widgets/flutter/selectable_text/text_selection.dart'; import 'package:flutter/cupertino.dart' hide TextSelectionGestureDetectorBuilder; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide SelectableText, TextSelectionGestureDetectorBuilder; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; class _TextSpanEditingController extends TextEditingController { _TextSpanEditingController({required TextSpan textSpan}) : _textSpan = textSpan, super(text: textSpan.toPlainText(includeSemanticsLabels: false)); final TextSpan _textSpan; @override TextSpan buildTextSpan({ required BuildContext context, TextStyle? style, required bool withComposing, }) { // This does not care about composing. return TextSpan(style: style, children: [_textSpan]); } @override set text(String? newText) { // This should never be reached. throw UnimplementedError(); } } class _SelectableTextSelectionGestureDetectorBuilder extends CustomTextSelectionGestureDetectorBuilder { _SelectableTextSelectionGestureDetectorBuilder({ required _SelectableTextState state, }) : _state = state, super(delegate: state); final _SelectableTextState _state; @override void onSingleTapUp(TapDragUpDetails details) { if (!delegate.selectionEnabled) { return; } super.onSingleTapUp(details); _state.widget.onTap?.call(); } } /// A run of selectable text with a single style. /// /// Consider using [SelectionArea] or [SelectableRegion] instead, which enable /// selection on a widget subtree, including but not limited to [Text] widgets. /// /// The [SelectableText] widget displays a string of text with a single style. /// The string might break across multiple lines or might all be displayed on /// the same line depending on the layout constraints. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} /// /// The [style] argument is optional. When omitted, the text will use the style /// from the closest enclosing [DefaultTextStyle]. If the given style's /// [TextStyle.inherit] property is true (the default), the given style will /// be merged with the closest enclosing [DefaultTextStyle]. This merging /// behavior is useful, for example, to make the text bold while using the /// default font family and size. /// /// {@macro flutter.material.textfield.wantKeepAlive} /// /// {@tool snippet} /// /// ```dart /// const SelectableText( /// 'Hello! How are you?', /// textAlign: TextAlign.center, /// style: TextStyle(fontWeight: FontWeight.bold), /// ) /// ``` /// {@end-tool} /// /// Using the [SelectableText.rich] constructor, the [SelectableText] widget can /// display a paragraph with differently styled [TextSpan]s. The sample /// that follows displays "Hello beautiful world" with different styles /// for each word. /// /// {@tool snippet} /// /// ```dart /// const SelectableText.rich( /// TextSpan( /// text: 'Hello', // default text style /// children: [ /// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), /// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), /// ], /// ), /// ) /// ``` /// {@end-tool} /// /// ## Interactivity /// /// To make [SelectableText] react to touch events, use callback [onTap] to achieve /// the desired behavior. /// /// ## Scrolling Considerations /// /// If this [SelectableText] is not a descendant of [Scaffold] and is being used /// within a [Scrollable] or nested [Scrollable]s, consider placing a /// [ScrollNotificationObserver] above the root [Scrollable] that contains this /// [SelectableText] to ensure proper scroll coordination for [SelectableText] /// and its components like [TextSelectionOverlay]. /// /// See also: /// /// * [Text], which is the non selectable version of this widget. /// * [TextField], which is the editable version of this widget. /// * [SelectionArea], which enables the selection of multiple [Text] widgets /// and of other widgets. class SelectableText extends StatefulWidget { /// Creates a selectable text widget. /// /// If the [style] argument is null, the text will use the style from the /// closest enclosing [DefaultTextStyle]. /// /// If the [showCursor], [autofocus], [dragStartBehavior], /// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are /// specified, the [maxLines] argument must be greater than zero. const SelectableText( String this.data, { super.key, this.focusNode, this.style, this.strutStyle, this.textAlign, this.textDirection, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) this.textScaleFactor, this.textScaler, this.showCursor = false, this.autofocus = false, @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.toolbarOptions, this.minLines, this.maxLines, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, this.cursorColor, this.selectionColor, this.selectionHeightStyle, this.selectionWidthStyle, this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection = true, this.selectionControls, this.onTap, this.scrollPhysics, this.scrollBehavior, this.semanticsLabel, this.textHeightBehavior, this.textWidthBasis, this.onSelectionChanged, this.contextMenuBuilder = _defaultContextMenuBuilder, this.magnifierConfiguration, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( textScaler == null || textScaleFactor == null, 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', ), textSpan = null; /// Creates a selectable text widget with a [TextSpan]. /// /// The [TextSpan.children] attribute of the [textSpan] parameter must only /// contain [TextSpan]s. Other types of [InlineSpan] are not allowed. const SelectableText.rich( TextSpan this.textSpan, { super.key, this.focusNode, this.style, this.strutStyle, this.textAlign, this.textDirection, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) this.textScaleFactor, this.textScaler, this.showCursor = false, this.autofocus = false, @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.toolbarOptions, this.minLines, this.maxLines, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, this.cursorColor, this.selectionColor, this.selectionHeightStyle, this.selectionWidthStyle, this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection = true, this.selectionControls, this.onTap, this.scrollPhysics, this.scrollBehavior, this.semanticsLabel, this.textHeightBehavior, this.textWidthBasis, this.onSelectionChanged, this.contextMenuBuilder = _defaultContextMenuBuilder, this.magnifierConfiguration, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( textScaler == null || textScaleFactor == null, 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', ), data = null; /// The text to display. /// /// This will be null if a [textSpan] is provided instead. final String? data; /// The text to display as a [TextSpan]. /// /// This will be null if [data] is provided instead. final TextSpan? textSpan; /// Defines the focus for this widget. /// /// Text is only selectable when widget is focused. /// /// The [focusNode] is a long-lived object that's typically managed by a /// [StatefulWidget] parent. See [FocusNode] for more information. /// /// To give the focus to this widget, provide a [focusNode] and then /// use the current [FocusScope] to request the focus: /// /// ```dart /// FocusScope.of(context).requestFocus(myFocusNode); /// ``` /// /// This happens automatically when the widget is tapped. /// /// To be notified when the widget gains or loses the focus, add a listener /// to the [focusNode]: /// /// ```dart /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); /// ``` /// /// If null, this widget will create its own [FocusNode] with /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget /// to be skipped over during focus traversal. final FocusNode? focusNode; /// The style to use for the text. /// /// If null, defaults [DefaultTextStyle] of context. final TextStyle? style; /// {@macro flutter.widgets.editableText.strutStyle} final StrutStyle? strutStyle; /// {@macro flutter.widgets.editableText.textAlign} final TextAlign? textAlign; /// {@macro flutter.widgets.editableText.textDirection} final TextDirection? textDirection; /// {@macro flutter.widgets.editableText.textScaleFactor} @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) final double? textScaleFactor; /// {@macro flutter.painting.textPainter.textScaler} final TextScaler? textScaler; /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; /// {@macro flutter.widgets.editableText.minLines} final int? minLines; /// {@macro flutter.widgets.editableText.maxLines} final int? maxLines; /// {@macro flutter.widgets.editableText.showCursor} final bool showCursor; /// {@macro flutter.widgets.editableText.cursorWidth} final double cursorWidth; /// {@macro flutter.widgets.editableText.cursorHeight} final double? cursorHeight; /// {@macro flutter.widgets.editableText.cursorRadius} final Radius? cursorRadius; /// The color of the cursor. /// /// The cursor indicates the current text insertion point. /// /// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also /// null and [ThemeData.platform] is [TargetPlatform.iOS] or /// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used. /// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used. final Color? cursorColor; /// The color to use when painting the selection. /// /// If this property is null, this widget gets the selection color from the /// inherited [DefaultSelectionStyle] (if any); if none, the selection /// color is derived from the [CupertinoThemeData.primaryColor] on /// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on /// other platforms. final Color? selectionColor; /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. final ui.BoxHeightStyle? selectionHeightStyle; /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. final ui.BoxWidthStyle? selectionWidthStyle; /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; /// {@macro flutter.widgets.editableText.selectionControls} final TextSelectionControls? selectionControls; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// Configuration of toolbar options. /// /// Paste and cut will be disabled regardless. /// /// If not set, select all and copy will be enabled by default. @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) final ToolbarOptions? toolbarOptions; /// {@macro flutter.widgets.editableText.selectionEnabled} bool get selectionEnabled => enableInteractiveSelection; /// Called when the user taps on this selectable text. /// /// The selectable text builds a [GestureDetector] to handle input events like tap, /// to trigger focus requests, to move the caret, adjust the selection, etc. /// Handling some of those events by wrapping the selectable text with a competing /// GestureDetector is problematic. /// /// To unconditionally handle taps, without interfering with the selectable text's /// internal gesture detector, provide this callback. /// /// To be notified when the text field gains or loses the focus, provide a /// [focusNode] and add a listener to that. /// /// To listen to arbitrary pointer events without competing with the /// selectable text's internal gesture detector, use a [Listener]. final GestureTapCallback? onTap; /// {@macro flutter.widgets.editableText.scrollPhysics} final ScrollPhysics? scrollPhysics; /// {@macro flutter.widgets.editableText.scrollBehavior} final ScrollBehavior? scrollBehavior; /// {@macro flutter.widgets.Text.semanticsLabel} final String? semanticsLabel; /// {@macro dart.ui.textHeightBehavior} final TextHeightBehavior? textHeightBehavior; /// {@macro flutter.painting.textPainter.textWidthBasis} final TextWidthBasis? textWidthBasis; /// {@macro flutter.widgets.editableText.onSelectionChanged} final SelectionChangedCallback? onSelectionChanged; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} final EditableTextContextMenuBuilder? contextMenuBuilder; static Widget _defaultContextMenuBuilder( BuildContext context, EditableTextState editableTextState, ) { return AdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, ); } /// The configuration for the magnifier used when the text is selected. /// /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] /// on Android, and builds nothing on all other platforms. To suppress the /// magnifier, consider passing [TextMagnifierConfiguration.disabled]. /// /// {@macro flutter.widgets.magnifier.intro} final TextMagnifierConfiguration? magnifierConfiguration; @override State createState() => _SelectableTextState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( DiagnosticsProperty('data', data, defaultValue: null), ) ..add( DiagnosticsProperty( 'semanticsLabel', semanticsLabel, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'focusNode', focusNode, defaultValue: null, ), ) ..add( DiagnosticsProperty('style', style, defaultValue: null), ) ..add( DiagnosticsProperty('autofocus', autofocus, defaultValue: false), ) ..add( DiagnosticsProperty( 'showCursor', showCursor, defaultValue: false, ), ) ..add(IntProperty('minLines', minLines, defaultValue: null)) ..add(IntProperty('maxLines', maxLines, defaultValue: null)) ..add( EnumProperty('textAlign', textAlign, defaultValue: null), ) ..add( EnumProperty( 'textDirection', textDirection, defaultValue: null, ), ) ..add( DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null), ) ..add( DiagnosticsProperty( 'textScaler', textScaler, defaultValue: null, ), ) ..add( DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), ) ..add( DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), ) ..add( DiagnosticsProperty( 'cursorRadius', cursorRadius, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'cursorColor', cursorColor, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'selectionColor', selectionColor, defaultValue: null, ), ) ..add( FlagProperty( 'selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled', ), ) ..add( DiagnosticsProperty( 'selectionControls', selectionControls, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollPhysics', scrollPhysics, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollBehavior', scrollBehavior, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'textHeightBehavior', textHeightBehavior, defaultValue: null, ), ); } } class _SelectableTextState extends State implements TextSelectionGestureDetectorBuilderDelegate { EditableTextState? get _editableText => editableTextKey.currentState; late _TextSpanEditingController _controller; FocusNode? _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true)); bool _showSelectionHandles = false; late _SelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; // API for TextSelectionGestureDetectorBuilderDelegate. @override late bool forcePressEnabled; @override final GlobalKey editableTextKey = GlobalKey(); @override bool get selectionEnabled => widget.selectionEnabled; // End of API for TextSelectionGestureDetectorBuilderDelegate. @override void initState() { super.initState(); _selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(state: this); _controller = _TextSpanEditingController( textSpan: widget.textSpan ?? TextSpan(text: widget.data), ); _controller.addListener(_onControllerChanged); _effectiveFocusNode.addListener(_handleFocusChanged); } @override void didUpdateWidget(SelectableText oldWidget) { super.didUpdateWidget(oldWidget); if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { _controller ..removeListener(_onControllerChanged) ..dispose(); _controller = _TextSpanEditingController( textSpan: widget.textSpan ?? TextSpan(text: widget.data), ); _controller.addListener(_onControllerChanged); } if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); } if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { _showSelectionHandles = false; } else { _showSelectionHandles = true; } } @override void dispose() { _effectiveFocusNode.removeListener(_handleFocusChanged); _focusNode?.dispose(); _controller.dispose(); super.dispose(); } void _onControllerChanged() { final bool showSelectionHandles = !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed; if (showSelectionHandles == _showSelectionHandles) { return; } setState(() { _showSelectionHandles = showSelectionHandles; }); } void _handleFocusChanged() { if (!_effectiveFocusNode.hasFocus && SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) { // We should only clear the selection when this SelectableText loses // focus while the application is currently running. It is possible // that the application is not currently running, for example on desktop // platforms, clicking on a different window switches the focus to // the new window causing the Flutter application to go inactive. In this // case we want to retain the selection so it remains when we return to // the Flutter application. _controller.value = TextEditingValue(text: _controller.value.text); } } void _handleSelectionChanged( TextSelection selection, SelectionChangedCause? cause, ) { final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); if (willShowSelectionHandles != _showSelectionHandles) { setState(() { _showSelectionHandles = willShowSelectionHandles; }); } widget.onSelectionChanged?.call(selection, cause); switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: if (cause == SelectionChangedCause.longPress) { _editableText?.bringIntoView(selection.base); } return; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: // Do nothing. } } /// Toggle the toolbar when a selection handle is tapped. void _handleSelectionHandleTapped() { if (_controller.selection.isCollapsed) { _editableText!.toggleToolbar(); } } bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { // When the text field is activated by something that doesn't trigger the // selection overlay, we shouldn't show the handles either. if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) { return false; } if (_controller.selection.isCollapsed) { return false; } if (cause == SelectionChangedCause.keyboard) { return false; } if (cause == SelectionChangedCause.longPress) { return true; } if (_controller.text.isNotEmpty) { return true; } return false; } @override Widget build(BuildContext context) { // TODO(garyq): Assert to block WidgetSpans from being used here are removed, // but we still do not yet have nice handling of things like carets, clipboard, // and other features. We should add proper support. Currently, caret handling // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010 // should be landed in SkParagraph after the switch is complete. assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasDirectionality(context)); assert( !(widget.style != null && !widget.style!.inherit && (widget.style!.fontSize == null || widget.style!.textBaseline == null)), 'inherit false style must supply fontSize and textBaseline', ); final ThemeData theme = Theme.of(context); final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of( context, ); final FocusNode focusNode = _effectiveFocusNode; TextSelectionControls? textSelectionControls = widget.selectionControls; final bool paintCursorAboveText; final bool cursorOpacityAnimates; Offset? cursorOffset; final Color cursorColor; final Color selectionColor; Radius? cursorRadius = widget.cursorRadius; switch (theme.platform) { case TargetPlatform.iOS: final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); forcePressEnabled = true; textSelectionControls ??= cupertinoTextSelectionHandleControls; paintCursorAboveText = true; cursorOpacityAnimates = true; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0, ); case TargetPlatform.macOS: final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); forcePressEnabled = false; textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls; paintCursorAboveText = true; cursorOpacityAnimates = true; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0, ); case TargetPlatform.android: case TargetPlatform.fuchsia: forcePressEnabled = false; textSelectionControls ??= materialTextSelectionHandleControls; paintCursorAboveText = false; cursorOpacityAnimates = false; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withValues(alpha: 0.40); case TargetPlatform.linux: case TargetPlatform.windows: forcePressEnabled = false; textSelectionControls ??= desktopTextSelectionHandleControls; paintCursorAboveText = false; cursorOpacityAnimates = false; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withValues(alpha: 0.40); } final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); TextStyle? effectiveTextStyle = widget.style; if (effectiveTextStyle == null || effectiveTextStyle.inherit) { effectiveTextStyle = defaultTextStyle.style.merge( widget.style ?? _controller._textSpan.style, ); } final TextScaler? effectiveScaler = widget.textScaler ?? switch (widget.textScaleFactor) { null => null, final double textScaleFactor => TextScaler.linear(textScaleFactor), }; final Widget child = RepaintBoundary( child: EditableText( key: editableTextKey, style: effectiveTextStyle, readOnly: true, toolbarOptions: widget.toolbarOptions, textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, showSelectionHandles: _showSelectionHandles, showCursor: widget.showCursor, controller: _controller, focusNode: focusNode, strutStyle: widget.strutStyle ?? const StrutStyle(), textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, textDirection: widget.textDirection, textScaler: effectiveScaler, autofocus: widget.autofocus, forceLine: false, minLines: widget.minLines, maxLines: widget.maxLines ?? defaultTextStyle.maxLines, selectionColor: widget.selectionColor ?? selectionColor, selectionControls: widget.selectionEnabled ? textSelectionControls : null, onSelectionChanged: _handleSelectionChanged, onSelectionHandleTapped: _handleSelectionHandleTapped, rendererIgnoresPointer: true, cursorWidth: widget.cursorWidth, cursorHeight: widget.cursorHeight, cursorRadius: cursorRadius, cursorColor: cursorColor, selectionHeightStyle: widget.selectionHeightStyle, selectionWidthStyle: widget.selectionWidthStyle, cursorOpacityAnimates: cursorOpacityAnimates, cursorOffset: cursorOffset, paintCursorAboveText: paintCursorAboveText, backgroundCursorColor: CupertinoColors.inactiveGray, enableInteractiveSelection: widget.enableInteractiveSelection, magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, dragStartBehavior: widget.dragStartBehavior, scrollPhysics: widget.scrollPhysics, scrollBehavior: widget.scrollBehavior, autofillHints: null, contextMenuBuilder: widget.contextMenuBuilder, ), ); return Semantics( label: widget.semanticsLabel, excludeSemantics: widget.semanticsLabel != null, onLongPress: () { _effectiveFocusNode.requestFocus(); }, child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: child, ), ); } } ================================================ FILE: lib/common/widgets/flutter/selectable_text/selection_area.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_region.dart'; import 'package:flutter/cupertino.dart' hide SelectableRegion, SelectableRegionState, SelectableRegionContextMenuBuilder; import 'package:flutter/material.dart' hide SelectionArea, SelectableRegion, SelectableRegionState, SelectableRegionContextMenuBuilder; import 'package:flutter/rendering.dart'; /// A widget that introduces an area for user selections with adaptive selection /// controls. /// /// This widget creates a [SelectableRegion] with platform-adaptive selection /// controls. /// /// Flutter widgets are not selectable by default. To enable selection for /// a specific screen, consider wrapping the body of the [Route] with a /// [SelectionArea]. /// /// The [SelectionArea] widget must have a [Localizations] ancestor that /// contains a [MaterialLocalizations] delegate; using the [MaterialApp] widget /// ensures that such an ancestor is present. /// /// {@tool dartpad} /// This example shows how to make a screen selectable. /// /// ** See code in examples/api/lib/material/selection_area/selection_area.0.dart ** /// {@end-tool} /// /// See also: /// /// * [SelectableRegion], which provides an overview of the selection system. /// * [SelectableText], which enables selection on a single run of text. /// * [SelectionListener], which enables accessing the [SelectionDetails] of /// the selectable subtree it wraps. class SelectionArea extends StatefulWidget { /// Creates a [SelectionArea]. /// /// If [selectionControls] is null, a platform specific one is used. const SelectionArea({ super.key, this.focusNode, this.selectionControls, this.contextMenuBuilder = _defaultContextMenuBuilder, this.magnifierConfiguration, this.onSelectionChanged, required this.child, }); /// The configuration for the magnifier in the selection region. /// /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] /// on Android, and builds nothing on all other platforms. To suppress the /// magnifier, consider passing [TextMagnifierConfiguration.disabled]. /// /// {@macro flutter.widgets.magnifier.intro} final TextMagnifierConfiguration? magnifierConfiguration; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// The delegate to build the selection handles and toolbar. /// /// If it is null, the platform specific selection control is used. final TextSelectionControls? selectionControls; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} /// /// If not provided, will build a default menu based on the ambient /// [ThemeData.platform]. /// /// {@tool dartpad} /// This example shows how to build a custom context menu for any selected /// content in a SelectionArea. /// /// ** See code in examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart ** /// {@end-tool} /// /// See also: /// /// * [AdaptiveTextSelectionToolbar], which is built by default. final SelectableRegionContextMenuBuilder? contextMenuBuilder; /// Called when the selected content changes. final ValueChanged? onSelectionChanged; /// The child widget this selection area applies to. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; static Widget _defaultContextMenuBuilder( BuildContext context, SelectableRegionState selectableRegionState, ) => AdaptiveTextSelectionToolbar.buttonItems( buttonItems: selectableRegionState.contextMenuButtonItems, anchors: selectableRegionState.contextMenuAnchors, ); @override State createState() => SelectionAreaState(); } /// State for a [SelectionArea]. class SelectionAreaState extends State { final GlobalKey _selectableRegionKey = GlobalKey(); /// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps. SelectableRegionState get selectableRegion => _selectableRegionKey.currentState!; @protected @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); final TextSelectionControls controls = widget.selectionControls ?? switch (Theme.of(context).platform) { TargetPlatform.android || TargetPlatform.fuchsia => materialTextSelectionHandleControls, TargetPlatform.linux || TargetPlatform.windows => desktopTextSelectionHandleControls, TargetPlatform.iOS => cupertinoTextSelectionHandleControls, TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls, }; return SelectableRegion( key: _selectableRegionKey, selectionControls: controls, focusNode: widget.focusNode, contextMenuBuilder: widget.contextMenuBuilder, magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, onSelectionChanged: widget.onSelectionChanged, child: widget.child, ); } } ================================================ FILE: lib/common/widgets/flutter/selectable_text/tap_and_drag.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' hide TapAndHorizontalDragGestureRecognizer; // Examples can assume: // void setState(VoidCallback fn) { } // late String _last; double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) { assert(originPosition != null); final Offset offset = event.position - originPosition!.global; return offset.distance; } // The possible states of a [BaseTapAndDragGestureRecognizer]. // // The recognizer advances from [ready] to [possible] when it starts tracking // a pointer in [BaseTapAndDragGestureRecognizer.addAllowedPointer]. Where it advances // from there depends on the sequence of pointer events that is tracked by the // recognizer, following the initial [PointerDownEvent]: // // * If a [PointerUpEvent] has not been tracked, the recognizer stays in the [possible] // state as long as it continues to track a pointer. // * If a [PointerMoveEvent] is tracked that has moved a sufficient global distance // from the initial [PointerDownEvent] and it came before a [PointerUpEvent], then // this recognizer moves from the [possible] state to [accepted]. // * If a [PointerUpEvent] is tracked before the pointer has moved a sufficient global // distance to be considered a drag, then this recognizer moves from the [possible] // state to [ready]. // * If a [PointerCancelEvent] is tracked then this recognizer moves from its current // state to [ready]. // // Once the recognizer has stopped tracking any remaining pointers, the recognizer // returns to the [ready] state. enum _DragState { // The recognizer is ready to start recognizing a drag. ready, // The sequence of pointer events seen thus far is consistent with a drag but // it has not been accepted definitively. possible, // The sequence of pointer events has been accepted definitively as a drag. accepted, } // A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps // that occur in a series of [PointerEvent]s and the most recent set of // [LogicalKeyboardKey]s pressed on the most recent tap down. // // A tap is tracked as part of a series of taps if: // // 1. The elapsed time between when a [PointerUpEvent] and the subsequent // [PointerDownEvent] does not exceed [kDoubleTapTimeout]. // 2. The delta between the position tapped in the global coordinate system // and the position that was tapped previously must be less than or equal // to [kDoubleTapSlop]. // // This mixin's state, i.e. the series of taps being tracked is reset when // a tap is tracked that does not meet any of the specifications stated above. mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { // Public state available to [OneSequenceGestureRecognizer]. // The [PointerDownEvent] that was most recently tracked in [addAllowedPointer]. // // This value will be null if a [PointerDownEvent] has not been tracked yet in // [addAllowedPointer] or the timer between two taps has elapsed. // // This value is only reset when the timer between a [PointerUpEvent] and the // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in // [addAllowedPointer]. PointerDownEvent? get currentDown => _down; // The [PointerUpEvent] that was most recently tracked in [handleEvent]. // // This value will be null if a [PointerUpEvent] has not been tracked yet in // [handleEvent] or the timer between two taps has elapsed. // // This value is only reset when the timer between a [PointerUpEvent] and the // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in // [addAllowedPointer]. PointerUpEvent? get currentUp => _up; // The number of consecutive taps that the most recently tracked [PointerDownEvent] // in [currentDown] represents. // // This value defaults to zero, meaning a tap series is not currently being tracked. // // When this value is greater than zero it means [addAllowedPointer] has run // and at least one [PointerDownEvent] belongs to the current series of taps // being tracked. // // [addAllowedPointer] will either increment this value by `1` or set the value to `1` // depending if the new [PointerDownEvent] is determined to be in the same series as the // tap that preceded it. If too much time has elapsed between two taps, the recognizer has lost // in the arena, the gesture has been cancelled, or the recognizer is being disposed then // this value will be set to `0`, and a new series will begin. int get consecutiveTapCount => _consecutiveTapCount; // The upper limit for the [consecutiveTapCount]. When this limit is reached // all tap related state is reset and a new tap series is tracked. // // If this value is null, [consecutiveTapCount] can grow infinitely large. int? get maxConsecutiveTap; // Private tap state tracked. PointerDownEvent? _down; PointerUpEvent? _up; int _consecutiveTapCount = 0; OffsetPair? _originPosition; int? _previousButtons; // For timing taps. Timer? _consecutiveTapTimer; Offset? _lastTapOffset; /// {@macro flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart} VoidCallback? onTapTrackStart; /// {@macro flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset} VoidCallback? onTapTrackReset; // When tracking a tap, the [consecutiveTapCount] is incremented if the given tap // falls under the tolerance specifications and reset to 1 if not. @override void addAllowedPointer(PointerDownEvent event) { super.addAllowedPointer(event); if (_consecutiveTapTimer != null && !_consecutiveTapTimer!.isActive) { _tapTrackerReset(); } if (maxConsecutiveTap == _consecutiveTapCount) { _tapTrackerReset(); } _up = null; if (_down != null && !_representsSameSeries(event)) { // The given tap does not match the specifications of the series of taps being tracked, // reset the tap count and related state. _consecutiveTapCount = 1; } else { _consecutiveTapCount += 1; } _consecutiveTapTimerStop(); // `_down` must be assigned in this method instead of [handleEvent], // because [acceptGesture] might be called before [handleEvent], // which may rely on `_down` to initiate a callback. _trackTap(event); } @override void handleEvent(PointerEvent event) { if (event is PointerMoveEvent) { final double computedSlop = computeHitSlop(event.kind, gestureSettings); final bool isSlopPastTolerance = _getGlobalDistance(event, _originPosition) > computedSlop; if (isSlopPastTolerance) { _consecutiveTapTimerStop(); _previousButtons = null; _lastTapOffset = null; } } else if (event is PointerUpEvent) { _up = event; if (_down != null) { _consecutiveTapTimerStop(); _consecutiveTapTimerStart(); } } else if (event is PointerCancelEvent) { _tapTrackerReset(); } } @override void rejectGesture(int pointer) { _tapTrackerReset(); } @override void dispose() { _tapTrackerReset(); super.dispose(); } void _trackTap(PointerDownEvent event) { _down = event; _previousButtons = event.buttons; _lastTapOffset = event.position; _originPosition = OffsetPair( local: event.localPosition, global: event.position, ); onTapTrackStart?.call(); } bool _hasSameButton(int buttons) { assert(_previousButtons != null); if (buttons == _previousButtons!) { return true; } else { return false; } } bool _isWithinConsecutiveTapTolerance(Offset secondTapOffset) { if (_lastTapOffset == null) { return false; } final Offset difference = secondTapOffset - _lastTapOffset!; return difference.distance <= kDoubleTapSlop; } bool _representsSameSeries(PointerDownEvent event) { return _consecutiveTapTimer != null && _isWithinConsecutiveTapTolerance(event.position) && _hasSameButton(event.buttons); } void _consecutiveTapTimerStart() { _consecutiveTapTimer ??= Timer( kDoubleTapTimeout, _consecutiveTapTimerTimeout, ); } void _consecutiveTapTimerStop() { if (_consecutiveTapTimer != null) { _consecutiveTapTimer!.cancel(); _consecutiveTapTimer = null; } } void _consecutiveTapTimerTimeout() { // The consecutive tap timer may time out before a tap down/tap up event is // fired. In this case we should not reset the tap tracker state immediately. // Instead we should reset the tap tracker on the next call to [addAllowedPointer], // if the timer is no longer active. } void _tapTrackerReset() { // The timer has timed out, i.e. the time between a [PointerUpEvent] and the subsequent // [PointerDownEvent] exceeded the duration of [kDoubleTapTimeout], so the tap belonging // to the [PointerDownEvent] cannot be considered part of the same tap series as the // previous [PointerUpEvent]. _consecutiveTapTimerStop(); _previousButtons = null; _originPosition = null; _lastTapOffset = null; _consecutiveTapCount = 0; _down = null; _up = null; onTapTrackReset?.call(); } } /// A base class for gesture recognizers that recognize taps and movements. /// /// Takes on the responsibilities of [TapGestureRecognizer] and /// [DragGestureRecognizer] in one [GestureRecognizer]. /// /// ### Gesture arena behavior /// /// [BaseTapAndDragGestureRecognizer] competes on the pointer events of /// [kPrimaryButton] only when it has at least one non-null `onTap*` /// or `onDrag*` callback. /// /// It will declare defeat if it determines that a gesture is not a /// tap (e.g. if the pointer is dragged too far while it's contacting the /// screen) or a drag (e.g. if the pointer was not dragged far enough to /// be considered a drag. /// /// This recognizer will not immediately declare victory for every tap that it /// recognizes, but it declares victory for every drag. /// /// The recognizer will declare victory when all other recognizer's in /// the arena have lost, if the timer of [kPressTimeout] elapses and a tap /// series greater than 1 is being tracked, or until the pointer has moved /// a sufficient global distance from the origin to be considered a drag. /// /// If this recognizer loses the arena (either by declaring defeat or by /// another recognizer declaring victory) while the pointer is contacting the /// screen, it will fire [onCancel] instead of [onTapUp] or [onDragEnd]. /// /// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer` /// /// Similar to [TapGestureRecognizer] and [DragGestureRecognizer], /// [BaseTapAndDragGestureRecognizer] will not aggressively declare victory when /// it detects a tap, so when it is competing with those gesture recognizers and /// others it has a chance of losing. Similarly, when `eagerVictoryOnDrag` is set /// to `false`, this recognizer will not aggressively declare victory when it /// detects a drag. By default, `eagerVictoryOnDrag` is set to `true`, so this /// recognizer will aggressively declare victory when it detects a drag. /// /// When competing against [TapGestureRecognizer], if the pointer does not move past the tap /// tolerance, then the recognizer that entered the arena first will win. In this case the /// gesture detected is a tap. If the pointer does travel past the tap tolerance then this /// recognizer will be declared winner by default. The gesture detected in this case is a drag. /// /// When competing against [DragGestureRecognizer], if the pointer does not move a sufficient /// global distance to be considered a drag, the recognizers will tie in the arena. If the /// pointer does travel enough distance then the recognizer that entered the arena /// first will win. The gesture detected in this case is a drag. /// /// {@tool dartpad} /// This example shows how to use the [TapAndPanGestureRecognizer] along with a /// [RawGestureDetector] to scale a Widget. /// /// ** See code in examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart ** /// {@end-tool} /// /// {@tool snippet} /// /// This example shows how to hook up [TapAndPanGestureRecognizer]s' to nested /// [RawGestureDetector]s'. It assumes that the code is being used inside a [State] /// object with a `_last` field that is then displayed as the child of the gesture detector. /// /// In this example, if the pointer has moved past the drag threshold, then the /// the first [TapAndPanGestureRecognizer] instance to receive the [PointerEvent] /// will win the arena because the recognizer will immediately declare victory. /// /// The first one to receive the event in the example will depend on where on both /// containers the pointer lands first. If your pointer begins in the overlapping /// area of both containers, then the inner-most widget will receive the event first. /// If your pointer begins in the yellow container then it will be the first to /// receive the event. /// /// If the pointer has not moved past the drag threshold, then the first recognizer /// to enter the arena will win (i.e. they both tie and the gesture arena will call /// [GestureArenaManager.sweep] so the first member of the arena will win). /// /// ```dart /// RawGestureDetector( /// gestures: { /// TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers( /// () => TapAndPanGestureRecognizer(), /// (TapAndPanGestureRecognizer instance) { /// instance /// ..onTapDown = (TapDragDownDetails details) { setState(() { _last = 'down_a'; }); } /// ..onDragStart = (TapDragStartDetails details) { setState(() { _last = 'drag_start_a'; }); } /// ..onDragUpdate = (TapDragUpdateDetails details) { setState(() { _last = 'drag_update_a'; }); } /// ..onDragEnd = (TapDragEndDetails details) { setState(() { _last = 'drag_end_a'; }); } /// ..onTapUp = (TapDragUpDetails details) { setState(() { _last = 'up_a'; }); } /// ..onCancel = () { setState(() { _last = 'cancel_a'; }); }; /// }, /// ), /// }, /// child: Container( /// width: 300.0, /// height: 300.0, /// color: Colors.yellow, /// alignment: Alignment.center, /// child: RawGestureDetector( /// gestures: { /// TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers( /// () => TapAndPanGestureRecognizer(), /// (TapAndPanGestureRecognizer instance) { /// instance /// ..onTapDown = (TapDragDownDetails details) { setState(() { _last = 'down_b'; }); } /// ..onDragStart = (TapDragStartDetails details) { setState(() { _last = 'drag_start_b'; }); } /// ..onDragUpdate = (TapDragUpdateDetails details) { setState(() { _last = 'drag_update_b'; }); } /// ..onDragEnd = (TapDragEndDetails details) { setState(() { _last = 'drag_end_b'; }); } /// ..onTapUp = (TapDragUpDetails details) { setState(() { _last = 'up_b'; }); } /// ..onCancel = () { setState(() { _last = 'cancel_b'; }); }; /// }, /// ), /// }, /// child: Container( /// width: 150.0, /// height: 150.0, /// color: Colors.blue, /// child: Text(_last), /// ), /// ), /// ), /// ) /// ``` /// {@end-tool} sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _TapStatusTrackerMixin { /// Creates a tap and drag gesture recognizer. /// /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} BaseTapAndDragGestureRecognizer({ super.debugOwner, super.supportedDevices, super.allowedButtonsFilter, this.eagerVictoryOnDrag = true, }) : _deadline = kPressTimeout, dragStartBehavior = DragStartBehavior.start; /// Configure the behavior of offsets passed to [onDragStart]. /// /// If set to [DragStartBehavior.start], the [onDragStart] callback will be called /// with the position of the pointer at the time this gesture recognizer won /// the arena. If [DragStartBehavior.down], [onDragStart] will be called with /// the position of the first detected down event for the pointer. When there /// are no other gestures competing with this gesture in the arena, there's /// no difference in behavior between the two settings. /// /// For more information about the gesture arena: /// https://flutter.dev/to/gesture-disambiguation /// /// By default, the drag start behavior is [DragStartBehavior.start]. /// /// See also: /// /// * [DragGestureRecognizer.dragStartBehavior], which includes more details and an example. DragStartBehavior dragStartBehavior; /// The frequency at which the [onDragUpdate] callback is called. /// /// The value defaults to null, meaning there is no delay for [onDragUpdate] callback. Duration? dragUpdateThrottleFrequency; /// An upper bound for the amount of taps that can belong to one tap series. /// /// When this limit is reached the series of taps being tracked by this /// recognizer will be reset. @override int? maxConsecutiveTap; /// Whether this recognizer eagerly declares victory when it has detected /// a drag. /// /// When this value is `false`, this recognizer will wait until it is the last /// recognizer in the gesture arena before declaring victory on a drag. /// /// Defaults to `true`. bool eagerVictoryOnDrag; /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapDown} /// /// This triggers after the down event, once a short timeout ([kPressTimeout]) has /// elapsed, or once the gestures has won the arena, whichever comes first. /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [TapDragDownDetails] object. /// /// {@template flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} /// The number of consecutive taps, and the keys that were pressed on tap down /// are also provided in the callback's `details` argument. /// {@endtemplate} /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [TapDragDownDetails], which is passed as an argument to this callback. GestureTapDragDownCallback? onTapDown; /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapUp} /// /// This triggers on the up event, if the recognizer wins the arena with it /// or has previously won. /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [TapDragUpDetails] object. /// /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [TapDragUpDetails], which is passed as an argument to this callback. GestureTapDragUpCallback? onTapUp; /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onStart} /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior] /// determines this position. /// /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [TapDragStartDetails], which is passed as an argument to this callback. GestureTapDragStartCallback? onDragStart; /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onUpdate} /// /// The distance traveled by the pointer since the last update is provided in /// the callback's `details` argument, which is a [TapDragUpdateDetails] object. /// /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [TapDragUpdateDetails], which is passed as an argument to this callback. GestureTapDragUpdateCallback? onDragUpdate; /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onEnd} /// /// The velocity is provided in the callback's `details` argument, which is a /// [TapDragEndDetails] object. /// /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [TapDragEndDetails], which is passed as an argument to this callback. GestureTapDragEndCallback? onDragEnd; /// The pointer that previously triggered [onTapDown] did not complete. /// /// This is called when a [PointerCancelEvent] is tracked when the [onTapDown] callback /// was previously called. /// /// It may also be called if a [PointerUpEvent] is tracked after the pointer has moved /// past the tap tolerance but not past the drag tolerance, and the recognizer has not /// yet won the arena. /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. GestureCancelCallback? onCancel; // Tap related state. bool _pastSlopTolerance = false; bool _sentTapDown = false; bool _wonArenaForPrimaryPointer = false; // Primary pointer being tracked by this recognizer. int? _primaryPointer; Timer? _deadlineTimer; // The recognizer will call [onTapDown] after this amount of time has elapsed // since starting to track the primary pointer. // // [onTapDown] will not be called if the primary pointer is // accepted, rejected, or all pointers are up or canceled before [_deadline]. final Duration _deadline; // Drag related state. _DragState _dragState = _DragState.ready; PointerEvent? _start; late OffsetPair _initialPosition; late OffsetPair _currentPosition; // late double _globalDistanceMoved; late double _globalDistanceMovedAllAxes; // For drag update throttle. TapDragUpdateDetails? _lastDragUpdateDetails; Timer? _dragUpdateThrottleTimer; final Set _acceptedActivePointers = {}; // Offset _getDeltaForDetails(Offset delta); // double? _getPrimaryValueFromOffset(Offset value); bool _hasSufficientGlobalDistanceToAccept( PointerDeviceKind pointerDeviceKind, ); // Drag updates may require throttling to avoid excessive updating, such as for text layouts in text // fields. The frequency of invocations is controlled by the [dragUpdateThrottleFrequency]. // // Once the drag gesture ends, any pending drag update will be fired // immediately. See [_checkDragEnd]. void _handleDragUpdateThrottled() { assert(_lastDragUpdateDetails != null); if (onDragUpdate != null) { invokeCallback( 'onDragUpdate', () => onDragUpdate!(_lastDragUpdateDetails!), ); } _dragUpdateThrottleTimer = null; _lastDragUpdateDetails = null; } @override bool isPointerAllowed(PointerEvent event) { if (_primaryPointer == null) { switch (event.buttons) { case kPrimaryButton: if (onTapDown == null && onDragStart == null && onDragUpdate == null && onDragEnd == null && onTapUp == null && onCancel == null) { return false; } default: return false; } } else { if (event.pointer != _primaryPointer) { return false; } } return super.isPointerAllowed(event as PointerDownEvent); } @override void addAllowedPointer(PointerDownEvent event) { if (_dragState == _DragState.ready) { super.addAllowedPointer(event); _primaryPointer = event.pointer; // _globalDistanceMoved = 0.0; _globalDistanceMovedAllAxes = 0.0; _dragState = _DragState.possible; _initialPosition = OffsetPair( global: event.position, local: event.localPosition, ); _currentPosition = _initialPosition; _deadlineTimer = Timer( _deadline, () => _didExceedDeadlineWithEvent(event), ); } } @override void handleNonAllowedPointer(PointerDownEvent event) { // There can be multiple drags simultaneously. Their effects are combined. if (event.buttons != kPrimaryButton) { if (!_wonArenaForPrimaryPointer) { super.handleNonAllowedPointer(event); } } } @override void acceptGesture(int pointer) { if (pointer != _primaryPointer) { return; } _stopDeadlineTimer(); assert(!_acceptedActivePointers.contains(pointer)); _acceptedActivePointers.add(pointer); // Called when this recognizer is accepted by the [GestureArena]. if (currentDown != null) { _checkTapDown(currentDown!); } _wonArenaForPrimaryPointer = true; // resolve(GestureDisposition.accepted) will be called when the [PointerMoveEvent] // has moved a sufficient global distance to be considered a drag and // `eagerVictoryOnDrag` is set to `true`. if (_start != null && eagerVictoryOnDrag) { assert(_dragState == _DragState.accepted); assert(currentUp == null); _acceptDrag(_start!); } // This recognizer will wait until it is the last one in the gesture arena // before accepting a drag when `eagerVictoryOnDrag` is set to `false`. if (_start != null && !eagerVictoryOnDrag) { assert(_dragState == _DragState.possible); assert(currentUp == null); _dragState = _DragState.accepted; _acceptDrag(_start!); } if (currentUp != null) { _checkTapUp(currentUp!); } } @override void didStopTrackingLastPointer(int pointer) { switch (_dragState) { case _DragState.ready: _checkCancel(); resolve(GestureDisposition.rejected); case _DragState.possible: if (_pastSlopTolerance) { // This means the pointer was not accepted as a tap. if (_wonArenaForPrimaryPointer) { // If the recognizer has already won the arena for the primary pointer being tracked // but the pointer has exceeded the tap tolerance, then the pointer is accepted as a // drag gesture. if (currentDown != null) { if (!_acceptedActivePointers.remove(pointer)) { resolvePointer(pointer, GestureDisposition.rejected); } _dragState = _DragState.accepted; _acceptDrag(currentDown!); _checkDragEnd(); } } else { _checkCancel(); resolve(GestureDisposition.rejected); } } else { // The pointer is accepted as a tap. if (currentUp != null) { _checkTapUp(currentUp!); } } case _DragState.accepted: // For the case when the pointer has been accepted as a drag. // Meaning [_checkTapDown] and [_checkDragStart] have already ran. _checkDragEnd(); } _stopDeadlineTimer(); _start = null; _dragState = _DragState.ready; _pastSlopTolerance = false; } @override void handleEvent(PointerEvent event) { if (event.pointer != _primaryPointer) { return; } super.handleEvent(event); if (event is PointerMoveEvent) { // Receiving a [PointerMoveEvent], does not automatically mean the pointer // being tracked is doing a drag gesture. There is some drift that can happen // between the initial [PointerDownEvent] and subsequent [PointerMoveEvent]s. // Accessing [_pastSlopTolerance] lets us know if our tap has moved past the // acceptable tolerance. If the pointer does not move past this tolerance than // it is not considered a drag. // // To be recognized as a drag, the [PointerMoveEvent] must also have moved // a sufficient global distance from the initial [PointerDownEvent] to be // accepted as a drag. This logic is handled in [_hasSufficientGlobalDistanceToAccept]. // // The recognizer will also detect the gesture as a drag when the pointer // has been accepted and it has moved past the [slopTolerance] but has not moved // a sufficient global distance from the initial position to be considered a drag. // In this case since the gesture cannot be a tap, it defaults to a drag. final double computedSlop = computeHitSlop(event.kind, gestureSettings); _pastSlopTolerance = _pastSlopTolerance || _getGlobalDistance(event, _initialPosition) > computedSlop; if (_dragState == _DragState.accepted) { _currentPosition = OffsetPair.fromEventPosition(event); _checkDragUpdate(event); } else if (_dragState == _DragState.possible) { if (_start == null) { // Only check for a drag if the start of a drag was not already identified. _checkDrag(event); } // This can occur when the recognizer is accepted before a [PointerMoveEvent] has been // received that moves the pointer a sufficient global distance to be considered a drag. if (_start != null && _wonArenaForPrimaryPointer) { _dragState = _DragState.accepted; _acceptDrag(_start!); } } } else if (event is PointerUpEvent) { if (_dragState == _DragState.possible) { // The drag has not been accepted before a [PointerUpEvent], therefore the recognizer // attempts to recognize a tap. stopTrackingIfPointerNoLongerDown(event); } else if (_dragState == _DragState.accepted) { _giveUpPointer(event.pointer); } } else if (event is PointerCancelEvent) { _dragState = _DragState.ready; _giveUpPointer(event.pointer); } } @override void rejectGesture(int pointer) { if (pointer != _primaryPointer) { return; } super.rejectGesture(pointer); _stopDeadlineTimer(); _giveUpPointer(pointer); _resetTaps(); _resetDragUpdateThrottle(); } @override void dispose() { _stopDeadlineTimer(); _resetDragUpdateThrottle(); super.dispose(); } @override String get debugDescription => 'tap_and_drag'; void _acceptDrag(PointerEvent event) { assert(_dragState == _DragState.accepted); if (!_wonArenaForPrimaryPointer) { return; } if (dragStartBehavior == DragStartBehavior.start) { _initialPosition += OffsetPair( global: event.delta, local: event.localDelta, ); _currentPosition = _initialPosition; } _checkDragStart(event); final Offset localDelta = event.localDelta; if (localDelta != Offset.zero) { _currentPosition = OffsetPair.fromEventPosition(event); final Offset correctedLocalPosition = _initialPosition.local + localDelta; final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!); final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions( transform: localToGlobalTransform, untransformedDelta: localDelta, untransformedEndPosition: correctedLocalPosition, ); final updateDelta = OffsetPair( local: localDelta, global: globalUpdateDelta, ); // Only adds delta for down behaviour _checkDragUpdate(event, corrected: _initialPosition + updateDelta); } } void _checkDrag(PointerMoveEvent event) { final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!); // final Offset movedLocally = _getDeltaForDetails(event.localDelta); // _globalDistanceMoved += // PointerEvent.transformDeltaViaPositions( // transform: localToGlobalTransform, // untransformedDelta: movedLocally, // untransformedEndPosition: event.localPosition, // ).distance * // (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign; _globalDistanceMovedAllAxes += PointerEvent.transformDeltaViaPositions( transform: localToGlobalTransform, untransformedDelta: event.localDelta, untransformedEndPosition: event.localPosition, ).distance * 1.sign; if (_hasSufficientGlobalDistanceToAccept(event.kind) || (_wonArenaForPrimaryPointer && _globalDistanceMovedAllAxes.abs() > computePanSlop(event.kind, gestureSettings))) { _start = event; if (eagerVictoryOnDrag) { _dragState = _DragState.accepted; if (!_wonArenaForPrimaryPointer) { resolve(GestureDisposition.accepted); } } } } void _checkTapDown(PointerDownEvent event) { if (_sentTapDown) { return; } final details = TapDragDownDetails( globalPosition: event.position, localPosition: event.localPosition, kind: getKindForPointer(event.pointer), consecutiveTapCount: consecutiveTapCount, ); if (onTapDown != null) { invokeCallback('onTapDown', () => onTapDown!(details)); } _sentTapDown = true; } void _checkTapUp(PointerUpEvent event) { if (!_wonArenaForPrimaryPointer) { return; } final upDetails = TapDragUpDetails( kind: event.kind, globalPosition: event.position, localPosition: event.localPosition, consecutiveTapCount: consecutiveTapCount, ); if (onTapUp != null) { invokeCallback('onTapUp', () => onTapUp!(upDetails)); } _resetTaps(); if (!_acceptedActivePointers.remove(event.pointer)) { resolvePointer(event.pointer, GestureDisposition.rejected); } } void _checkDragStart(PointerEvent event) { if (onDragStart != null) { final details = TapDragStartDetails( sourceTimeStamp: event.timeStamp, globalPosition: _initialPosition.global, localPosition: _initialPosition.local, kind: getKindForPointer(event.pointer), consecutiveTapCount: consecutiveTapCount, ); invokeCallback('onDragStart', () => onDragStart!(details)); } _start = null; } void _checkDragUpdate(PointerEvent event, {OffsetPair? corrected}) { final Offset globalPosition = corrected?.global ?? event.position; final Offset localPosition = corrected?.local ?? event.localPosition; final details = TapDragUpdateDetails( sourceTimeStamp: event.timeStamp, delta: event.localDelta, globalPosition: globalPosition, kind: getKindForPointer(event.pointer), localPosition: localPosition, offsetFromOrigin: globalPosition - _initialPosition.global, localOffsetFromOrigin: localPosition - _initialPosition.local, consecutiveTapCount: consecutiveTapCount, ); if (dragUpdateThrottleFrequency != null) { _lastDragUpdateDetails = details; // Only schedule a new timer if there's not one pending. _dragUpdateThrottleTimer ??= Timer( dragUpdateThrottleFrequency!, _handleDragUpdateThrottled, ); } else { if (onDragUpdate != null) { invokeCallback('onDragUpdate', () => onDragUpdate!(details)); } } } void _checkDragEnd() { final Offset globalPosition = _currentPosition.global; final Offset localPosition = _currentPosition.local; if (_dragUpdateThrottleTimer != null) { // If there's already an update scheduled, trigger it immediately and // cancel the timer. _dragUpdateThrottleTimer!.cancel(); _handleDragUpdateThrottled(); } final endDetails = TapDragEndDetails( globalPosition: globalPosition, localPosition: localPosition, primaryVelocity: 0.0, consecutiveTapCount: consecutiveTapCount, ); if (onDragEnd != null) { invokeCallback('onDragEnd', () => onDragEnd!(endDetails)); } _resetTaps(); _resetDragUpdateThrottle(); } void _checkCancel() { if (!_sentTapDown) { // Do not fire tap cancel if [onTapDown] was never called. return; } if (onCancel != null) { invokeCallback('onCancel', onCancel!); } _resetDragUpdateThrottle(); _resetTaps(); } void _didExceedDeadlineWithEvent(PointerDownEvent event) { _didExceedDeadline(); } void _didExceedDeadline() { if (currentDown != null) { _checkTapDown(currentDown!); if (consecutiveTapCount > 1) { // If our consecutive tap count is greater than 1, i.e. is a double tap or greater, // then this recognizer declares victory to prevent the [LongPressGestureRecognizer] // from declaring itself the winner if a double tap is held for too long. resolve(GestureDisposition.accepted); } } } void _giveUpPointer(int pointer) { stopTrackingPointer(pointer); // If the pointer was never accepted, then it is rejected since this recognizer is no longer // interested in winning the gesture arena for it. if (!_acceptedActivePointers.remove(pointer)) { resolvePointer(pointer, GestureDisposition.rejected); } } void _resetTaps() { _sentTapDown = false; _wonArenaForPrimaryPointer = false; _primaryPointer = null; } void _resetDragUpdateThrottle() { if (dragUpdateThrottleFrequency == null) { return; } _lastDragUpdateDetails = null; if (_dragUpdateThrottleTimer != null) { _dragUpdateThrottleTimer!.cancel(); _dragUpdateThrottleTimer = null; } } void _stopDeadlineTimer() { if (_deadlineTimer != null) { _deadlineTimer!.cancel(); _deadlineTimer = null; } } } /// Recognizes taps along with movement in the horizontal direction. /// /// Before this recognizer has won the arena for the primary pointer being tracked, /// it will only accept a drag on the horizontal axis. If a drag is detected after /// this recognizer has won the arena then it will accept a drag on any axis. /// /// See also: /// /// * [BaseTapAndDragGestureRecognizer], for the class that provides the main /// implementation details of this recognizer. /// * [TapAndPanGestureRecognizer], for a similar recognizer that accepts a drag /// on any axis regardless if the recognizer has won the arena for the primary /// pointer being tracked. /// * [HorizontalDragGestureRecognizer], for a similar recognizer that only recognizes /// horizontal movement. class TapAndHorizontalDragGestureRecognizer extends BaseTapAndDragGestureRecognizer { /// Create a gesture recognizer for interactions in the horizontal axis. /// /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} TapAndHorizontalDragGestureRecognizer({ super.debugOwner, super.supportedDevices, }); @override bool _hasSufficientGlobalDistanceToAccept( PointerDeviceKind pointerDeviceKind, ) { return false; // return _globalDistanceMoved.abs() > // computeHitSlop(pointerDeviceKind, gestureSettings); } // @override // Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0); // @override // double _getPrimaryValueFromOffset(Offset value) => value.dx; @override String get debugDescription => 'tap and horizontal drag'; } /// {@template flutter.gestures.selectionrecognizers.TapAndPanGestureRecognizer} /// Recognizes taps along with both horizontal and vertical movement. /// /// This recognizer will accept a drag on any axis, regardless if it has won the /// arena for the primary pointer being tracked. /// /// See also: /// /// * [BaseTapAndDragGestureRecognizer], for the class that provides the main /// implementation details of this recognizer. /// * [TapAndHorizontalDragGestureRecognizer], for a similar recognizer that /// only accepts horizontal drags before it has won the arena for the primary /// pointer being tracked. /// * [PanGestureRecognizer], for a similar recognizer that only recognizes /// movement. /// {@endtemplate} class TapAndPanGestureRecognizer extends BaseTapAndDragGestureRecognizer { /// Create a gesture recognizer for interactions on a plane. TapAndPanGestureRecognizer({super.debugOwner, super.supportedDevices}); @override bool _hasSufficientGlobalDistanceToAccept( PointerDeviceKind pointerDeviceKind, ) { return true; // return _globalDistanceMoved.abs() > // computePanSlop(pointerDeviceKind, gestureSettings); } // @override // Offset _getDeltaForDetails(Offset delta) => delta; // @override // double? _getPrimaryValueFromOffset(Offset value) => null; @override String get debugDescription => 'tap and pan'; } ================================================ FILE: lib/common/widgets/flutter/selectable_text/text.dart ================================================ import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/selection_area.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart' hide SelectableText, SelectionArea; Widget selectableText( String text, { TextStyle? style, }) { if (PlatformUtils.isDesktop) { return SelectionArea( child: Text( style: style, text, ), ); } return SelectableText( style: style, text, scrollPhysics: const NeverScrollableScrollPhysics(), ); } Widget selectableRichText( TextSpan textSpan, { TextStyle? style, }) { if (PlatformUtils.isDesktop) { return SelectionArea( child: Text.rich( style: style, textSpan, ), ); } return SelectableText.rich( style: style, textSpan, scrollPhysics: const NeverScrollableScrollPhysics(), ); } ================================================ FILE: lib/common/widgets/flutter/selectable_text/text_selection.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/selectable_text/tap_and_drag.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' hide BaseTapAndDragGestureRecognizer, TapAndHorizontalDragGestureRecognizer, TapAndPanGestureRecognizer; import 'package:flutter/material.dart' hide TextSelectionGestureDetector; class CustomTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { CustomTextSelectionGestureDetectorBuilder({required super.delegate}); @override Widget buildGestureDetector({ Key? key, HitTestBehavior? behavior, required Widget child, }) { return TextSelectionGestureDetector( key: key, onTapTrackStart: onTapTrackStart, onTapTrackReset: onTapTrackReset, onTapDown: onTapDown, onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, onSecondaryTap: onSecondaryTap, onSecondaryTapDown: onSecondaryTapDown, onSingleTapUp: onSingleTapUp, onSingleTapCancel: onSingleTapCancel, onUserTap: onUserTap, onSingleLongTapStart: onSingleLongTapStart, onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapEnd: onSingleLongTapEnd, onSingleLongTapCancel: onSingleLongTapCancel, onDoubleTapDown: onDoubleTapDown, onTripleTapDown: onTripleTapDown, onDragSelectionStart: onDragSelectionStart, onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionEnd: onDragSelectionEnd, onUserTapAlwaysCalled: onUserTapAlwaysCalled, behavior: behavior, child: child, ); } } /// A gesture detector to respond to non-exclusive event chains for a text field. /// /// An ordinary [GestureDetector] configured to handle events like tap and /// double tap will only recognize one or the other. This widget detects both: /// the first tap and then any subsequent taps that occurs within a time limit /// after the first. /// /// See also: /// /// * [TextField], a Material text field which uses this gesture detector. /// * [CupertinoTextField], a Cupertino text field which uses this gesture /// detector. class TextSelectionGestureDetector extends StatefulWidget { /// Create a [TextSelectionGestureDetector]. /// /// Multiple callbacks can be called for one sequence of input gesture. const TextSelectionGestureDetector({ super.key, this.onTapTrackStart, this.onTapTrackReset, this.onTapDown, this.onForcePressStart, this.onForcePressEnd, this.onSecondaryTap, this.onSecondaryTapDown, this.onSingleTapUp, this.onSingleTapCancel, this.onUserTap, this.onSingleLongTapStart, this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, this.onSingleLongTapCancel, this.onDoubleTapDown, this.onTripleTapDown, this.onDragSelectionStart, this.onDragSelectionUpdate, this.onDragSelectionEnd, this.onUserTapAlwaysCalled = false, this.behavior, required this.child, }); /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart} /// Callback used to indicate that a tap tracking has started upon /// a [PointerDownEvent]. /// {@endtemplate} final VoidCallback? onTapTrackStart; /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset} /// Callback used to indicate that a tap tracking has been reset which /// happens on the next [PointerDownEvent] after the timer between two taps /// elapses, the recognizer loses the arena, the gesture is cancelled or /// the recognizer is disposed of. /// {@endtemplate} final VoidCallback? onTapTrackReset; /// Called for every tap down including every tap down that's part of a /// double click or a long press, except touches that include enough movement /// to not qualify as taps (e.g. pans and flings). final GestureTapDragDownCallback? onTapDown; /// Called when a pointer has tapped down and the force of the pointer has /// just become greater than [ForcePressGestureRecognizer.startPressure]. final GestureForcePressStartCallback? onForcePressStart; /// Called when a pointer that had previously triggered [onForcePressStart] is /// lifted off the screen. final GestureForcePressEndCallback? onForcePressEnd; /// Called for a tap event with the secondary mouse button. final GestureTapCallback? onSecondaryTap; /// Called for a tap down event with the secondary mouse button. final GestureTapDownCallback? onSecondaryTapDown; /// Called for the first tap in a series of taps, consecutive taps do not call /// this method. /// /// For example, if the detector was configured with [onTapDown] and /// [onDoubleTapDown], three quick taps would be recognized as a single tap /// down, followed by a tap up, then a double tap down, followed by a single tap down. final GestureTapDragUpCallback? onSingleTapUp; /// Called for each touch that becomes recognized as a gesture that is not a /// short tap, such as a long tap or drag. It is called at the moment when /// another gesture from the touch is recognized. final GestureCancelCallback? onSingleTapCancel; /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is /// disabled, which is the default behavior. /// /// When [onUserTapAlwaysCalled] is enabled, this is called for every tap, /// including consecutive taps. final GestureTapCallback? onUserTap; /// Called for a single long tap that's sustained for longer than /// [kLongPressTimeout] but not necessarily lifted. Not called for a /// double-tap-hold, which calls [onDoubleTapDown] instead. final GestureLongPressStartCallback? onSingleLongTapStart; /// Called after [onSingleLongTapStart] when the pointer is dragged. final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; /// Called after [onSingleLongTapStart] when the pointer is lifted. final GestureLongPressEndCallback? onSingleLongTapEnd; /// Called after [onSingleLongTapStart] when the pointer is canceled. final GestureLongPressCancelCallback? onSingleLongTapCancel; /// Called after a momentary hold or a short tap that is close in space and /// time (within [kDoubleTapTimeout]) to a previous short tap. final GestureTapDragDownCallback? onDoubleTapDown; /// Called after a momentary hold or a short tap that is close in space and /// time (within [kDoubleTapTimeout]) to a previous double-tap. final GestureTapDragDownCallback? onTripleTapDown; /// Called when a mouse starts dragging to select text. final GestureTapDragStartCallback? onDragSelectionStart; /// Called repeatedly as a mouse moves while dragging. final GestureTapDragUpdateCallback? onDragSelectionUpdate; /// Called when a mouse that was previously dragging is released. final GestureTapDragEndCallback? onDragSelectionEnd; /// Whether [onUserTap] will be called for all taps including consecutive taps. /// /// Defaults to false, so [onUserTap] is only called for each distinct tap. final bool onUserTapAlwaysCalled; /// How this gesture detector should behave during hit testing. /// /// This defaults to [HitTestBehavior.deferToChild]. final HitTestBehavior? behavior; /// Child below this widget. final Widget child; @override State createState() => _TextSelectionGestureDetectorState(); } class _TextSelectionGestureDetectorState extends State { // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, // which can grow to be infinitely large, to a value between 1 and 3. The value // that the raw count is converted to is based on the default observed behavior // on the native platforms. // // This method should be used in all instances when details.consecutiveTapCount // would be used. static int _getEffectiveConsecutiveTapCount(int rawCount) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: // From observation, these platform's reset their tap count to 0 when // the number of consecutive taps exceeds 3. For example on Debian Linux // with GTK, when going past a triple click, on the fourth click the // selection is moved to the precise click position, on the fifth click // the word at the position is selected, and on the sixth click the // paragraph at the position is selected. return rawCount <= 3 ? rawCount : (rawCount % 3 == 0 ? 3 : rawCount % 3); case TargetPlatform.iOS: case TargetPlatform.macOS: // From observation, these platform's either hold their tap count at 3. // For example on macOS, when going past a triple click, the selection // should be retained at the paragraph that was first selected on triple // click. return math.min(rawCount, 3); case TargetPlatform.windows: // From observation, this platform's consecutive tap actions alternate // between double click and triple click actions. For example, after a // triple click has selected a paragraph, on the next click the word at // the clicked position will be selected, and on the next click the // paragraph at the position is selected. return rawCount < 2 ? rawCount : 2 + rawCount % 2; } } void _handleTapTrackStart() { widget.onTapTrackStart?.call(); } void _handleTapTrackReset() { widget.onTapTrackReset?.call(); } // The down handler is force-run on success of a single tap and optimistically // run before a long press success. void _handleTapDown(TapDragDownDetails details) { widget.onTapDown?.call(details); // This isn't detected as a double tap gesture in the gesture recognizer // because it's 2 single taps, each of which may do different things depending // on whether it's a single tap, the first tap of a double tap, the second // tap held down, a clean double tap etc. if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) { return widget.onDoubleTapDown?.call(details); } if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) { return widget.onTripleTapDown?.call(details); } } void _handleTapUp(TapDragUpDetails details) { if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) { widget.onSingleTapUp?.call(details); widget.onUserTap?.call(); } else if (widget.onUserTapAlwaysCalled) { widget.onUserTap?.call(); } } void _handleTapCancel() { widget.onSingleTapCancel?.call(); } void _handleDragStart(TapDragStartDetails details) { widget.onDragSelectionStart?.call(details); } void _handleDragUpdate(TapDragUpdateDetails details) { widget.onDragSelectionUpdate?.call(details); } void _handleDragEnd(TapDragEndDetails details) { widget.onDragSelectionEnd?.call(details); } void _forcePressStarted(ForcePressDetails details) { widget.onForcePressStart?.call(details); } void _forcePressEnded(ForcePressDetails details) { widget.onForcePressEnd?.call(details); } void _handleLongPressStart(LongPressStartDetails details) { widget.onSingleLongTapStart?.call(details); } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { widget.onSingleLongTapMoveUpdate?.call(details); } void _handleLongPressEnd(LongPressEndDetails details) { widget.onSingleLongTapEnd?.call(details); } void _handleLongPressCancel() { widget.onSingleLongTapCancel?.call(); } @override Widget build(BuildContext context) { final gestures = {}; gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { instance ..onSecondaryTap = widget.onSecondaryTap ..onSecondaryTapDown = widget.onSecondaryTapDown; }, ); if (widget.onSingleLongTapStart != null || widget.onSingleLongTapMoveUpdate != null || widget.onSingleLongTapEnd != null || widget.onSingleLongTapCancel != null) { gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( debugOwner: this, supportedDevices: {PointerDeviceKind.touch}, ), (LongPressGestureRecognizer instance) { instance ..onLongPressStart = _handleLongPressStart ..onLongPressMoveUpdate = _handleLongPressMoveUpdate ..onLongPressEnd = _handleLongPressEnd ..onLongPressCancel = _handleLongPressCancel; }, ); } if (widget.onDragSelectionStart != null || widget.onDragSelectionUpdate != null || widget.onDragSelectionEnd != null) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: gestures[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers< TapAndHorizontalDragGestureRecognizer >( () => TapAndHorizontalDragGestureRecognizer(debugOwner: this), (TapAndHorizontalDragGestureRecognizer instance) { instance // Text selection should start from the position of the first pointer // down event. ..dragStartBehavior = DragStartBehavior.down ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS ..onTapTrackStart = _handleTapTrackStart ..onTapTrackReset = _handleTapTrackReset ..onTapDown = _handleTapDown ..onDragStart = _handleDragStart ..onDragUpdate = _handleDragUpdate ..onDragEnd = _handleDragEnd ..onTapUp = _handleTapUp ..onCancel = _handleTapCancel; }, ); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: gestures[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapAndPanGestureRecognizer(debugOwner: this), (TapAndPanGestureRecognizer instance) { instance // Text selection should start from the position of the first pointer // down event. ..dragStartBehavior = DragStartBehavior.down ..onTapTrackStart = _handleTapTrackStart ..onTapTrackReset = _handleTapTrackReset ..onTapDown = _handleTapDown ..onDragStart = _handleDragStart ..onDragUpdate = _handleDragUpdate ..onDragEnd = _handleDragEnd ..onTapUp = _handleTapUp ..onCancel = _handleTapCancel; }, ); } } if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => ForcePressGestureRecognizer(debugOwner: this), (ForcePressGestureRecognizer instance) { instance ..onStart = widget.onForcePressStart != null ? _forcePressStarted : null ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null; }, ); } return RawGestureDetector( gestures: gestures, excludeFromSemantics: true, behavior: widget.behavior, child: widget.child, ); } } ================================================ FILE: lib/common/widgets/flutter/sliver_layout_builder.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart' hide ConstrainedLayoutBuilder, LayoutBuilder, RenderConstrainedLayoutBuilder; /// Builds a sliver widget tree that can depend on its own [SliverConstraints]. /// /// Similar to the [LayoutBuilder] widget except its builder should return a sliver /// widget, and [SliverLayoutBuilder] is itself a sliver. The framework calls the /// [builder] function at layout time and provides the current [SliverConstraints]. /// The [SliverLayoutBuilder]'s final [SliverGeometry] will match the [SliverGeometry] /// of its child. /// /// {@macro flutter.widgets.ConstrainedLayoutBuilder} /// /// See also: /// /// * [LayoutBuilder], the non-sliver version of this widget. class SliverLayoutBuilder extends ConstrainedLayoutBuilder { /// Creates a sliver widget that defers its building until layout. const SliverLayoutBuilder({super.key, required super.builder}); @override RenderConstrainedLayoutBuilder createRenderObject( BuildContext context, ) => _RenderSliverLayoutBuilder(); } class _RenderSliverLayoutBuilder extends RenderSliver with RenderObjectWithChildMixin, RenderObjectWithLayoutCallbackMixin, RenderConstrainedLayoutBuilder { @override double childMainAxisPosition(RenderObject child) { assert(child == this.child); return 0; } @override void performLayout() { runLayoutCallback(); child?.layout(constraints, parentUsesSize: true); geometry = child?.geometry ?? SliverGeometry.zero; } @override void applyPaintTransform(RenderObject child, Matrix4 transform) { assert(child == this.child); // child's offset is always (0, 0), transform.translate(0, 0) does not mutate the transform. } @override void paint(PaintingContext context, Offset offset) { // This renderObject does not introduce additional offset to child's position. if (child?.geometry?.visible ?? false) { context.paintChild(child!, offset); } } @override bool hitTestChildren( SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition, }) { return child != null && child!.geometry!.hitTestExtent > 0 && child!.hitTest( result, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition, ); } } ================================================ FILE: lib/common/widgets/flutter/tabs.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show SemanticsRole; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart' hide TabBarView; /// A page view that displays the widget which corresponds to the currently /// selected tab. /// /// This widget is typically used in conjunction with a [TabBar]. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40} /// /// If a [TabController] is not provided, then there must be a [DefaultTabController] /// ancestor. /// /// The tab controller's [TabController.length] must equal the length of the /// [children] list and the length of the [TabBar.tabs] list. /// /// To see a sample implementation, visit the [TabController] documentation. class CustomTabBarView extends StatefulWidget { /// Creates a page view with one child per tab. /// /// The length of [children] must be the same as the [controller]'s length. const CustomTabBarView({ super.key, required this.children, this.controller, this.physics, this.dragStartBehavior = DragStartBehavior.start, this.viewportFraction = 1.0, this.clipBehavior = Clip.hardEdge, this.scrollDirection = Axis.horizontal, }); /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController? controller; /// One widget per tab. /// /// Its length must match the length of the [TabBar.tabs] /// list, as well as the [controller]'s [TabController.length]. final List children; /// How the page view should respond to user input. /// /// For example, determines how the page view continues to animate after the /// user stops dragging the page view. /// /// The physics are modified to snap to page boundaries using /// [PageScrollPhysics] prior to being used. /// /// Defaults to matching platform conventions. final ScrollPhysics? physics; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.pageview.viewportFraction} final double viewportFraction; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; final Axis scrollDirection; @override State createState() => _CustomTabBarViewState(); } class _CustomTabBarViewState extends State { TabController? _controller; PageController? _pageController; late List _childrenWithKey; int? _currentIndex; int _warpUnderwayCount = 0; int _scrollUnderwayCount = 0; bool _debugHasScheduledValidChildrenCountCheck = false; // If the TabBarView is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController? newController = widget.controller ?? DefaultTabController.maybeOf(context); assert(() { if (newController == null) { throw FlutterError( 'No TabController for ${widget.runtimeType}.\n' 'When creating a ${widget.runtimeType}, you must either provide an explicit ' 'TabController using the "controller" property, or you must ensure that there ' 'is a DefaultTabController above the ${widget.runtimeType}.\n' 'In this case, there was neither an explicit controller nor a default controller.', ); } return true; }()); if (newController == _controller) { return; } if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = newController; if (_controller != null) { _controller!.animation!.addListener(_handleTabControllerAnimationTick); } } void _jumpToPage(int page) { _warpUnderwayCount += 1; _pageController!.jumpToPage(page); _warpUnderwayCount -= 1; } Future _animateToPage( int page, { required Duration duration, required Curve curve, }) async { _warpUnderwayCount += 1; await _pageController!.animateToPage( page, duration: duration, curve: curve, ); _warpUnderwayCount -= 1; } @override void initState() { super.initState(); _updateChildren(); } @override void didChangeDependencies() { super.didChangeDependencies(); _updateTabController(); _currentIndex = _controller!.index; if (_pageController == null) { _pageController = PageController( initialPage: _currentIndex!, viewportFraction: widget.viewportFraction, ); } else { _pageController!.jumpToPage(_currentIndex!); } } @override void didUpdateWidget(CustomTabBarView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _updateTabController(); _currentIndex = _controller!.index; _jumpToPage(_currentIndex!); } if (widget.viewportFraction != oldWidget.viewportFraction) { _pageController?.dispose(); _pageController = PageController( initialPage: _currentIndex!, viewportFraction: widget.viewportFraction, ); } // While a warp is under way, we stop updating the tab page contents. // This is tracked in https://github.com/flutter/flutter/issues/31269. if (widget.children != oldWidget.children && _warpUnderwayCount == 0) { _updateChildren(); } } @override void dispose() { if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = null; _pageController?.dispose(); // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } void _updateChildren() { _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList( widget.children.map((Widget child) { return Semantics(role: SemanticsRole.tabPanel, child: child); }).toList(), ); } void _handleTabControllerAnimationTick() { if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) { return; } // This widget is driving the controller's animation. if (_controller!.index != _currentIndex) { _currentIndex = _controller!.index; _warpToCurrentIndex(); } } void _warpToCurrentIndex() { if (!mounted || _pageController!.page == _currentIndex!.toDouble()) { return; } final bool adjacentDestination = (_currentIndex! - _controller!.previousIndex).abs() == 1; if (adjacentDestination) { _warpToAdjacentTab(_controller!.animationDuration); } else { _warpToNonAdjacentTab(_controller!.animationDuration); } } Future _warpToAdjacentTab(Duration duration) async { if (duration == Duration.zero) { _jumpToPage(_currentIndex!); } else { await _animateToPage( _currentIndex!, duration: duration, curve: Curves.ease, ); } if (mounted) { setState(_updateChildren); } return Future.value(); } Future _warpToNonAdjacentTab(Duration duration) async { final int previousIndex = _controller!.previousIndex; assert((_currentIndex! - previousIndex).abs() > 1); // initialPage defines which page is shown when starting the animation. // This page is adjacent to the destination page. final int initialPage = _currentIndex! > previousIndex ? _currentIndex! - 1 : _currentIndex! + 1; setState(() { // Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children. // For motivation, see https://github.com/flutter/flutter/pull/29188 and // https://github.com/flutter/flutter/issues/27010#issuecomment-486475152. _childrenWithKey = List.of(_childrenWithKey, growable: false); final Widget temp = _childrenWithKey[initialPage]; _childrenWithKey[initialPage] = _childrenWithKey[previousIndex]; _childrenWithKey[previousIndex] = temp; }); // Make a first jump to the adjacent page. _jumpToPage(initialPage); // Jump or animate to the destination page. if (duration == Duration.zero) { _jumpToPage(_currentIndex!); } else { await _animateToPage( _currentIndex!, duration: duration, curve: Curves.ease, ); } if (mounted) { setState(_updateChildren); } } void _syncControllerOffset() { _controller!.offset = clampDouble( _pageController!.page! - _controller!.index, -1.0, 1.0, ); } // Called when the PageView scrolls bool _handleScrollNotification(ScrollNotification notification) { if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) { return false; } if (notification.depth != 0) { return false; } if (!_controllerIsValid) { return false; } _scrollUnderwayCount += 1; final double page = _pageController!.page!; if (notification is ScrollUpdateNotification && !_controller!.indexIsChanging) { final bool pageChanged = (page - _controller!.index).abs() > 1.0; if (pageChanged) { _controller!.index = page.round(); _currentIndex = _controller!.index; } _syncControllerOffset(); } else if (notification is ScrollEndNotification) { _controller!.index = page.round(); _currentIndex = _controller!.index; if (!_controller!.indexIsChanging) { _syncControllerOffset(); } } _scrollUnderwayCount -= 1; return false; } bool _debugScheduleCheckHasValidChildrenCount() { if (_debugHasScheduledValidChildrenCountCheck) { return true; } WidgetsBinding.instance.addPostFrameCallback((Duration duration) { _debugHasScheduledValidChildrenCountCheck = false; if (!mounted) { return; } assert(() { if (_controller!.length != widget.children.length) { throw FlutterError( "Controller's length property (${_controller!.length}) does not match the " "number of children (${widget.children.length}) present in TabBarView's children property.", ); } return true; }()); }, debugLabel: 'TabBarView.validChildrenCountCheck'); _debugHasScheduledValidChildrenCountCheck = true; return true; } @override Widget build(BuildContext context) { assert(_debugScheduleCheckHasValidChildrenCount()); return NotificationListener( onNotification: _handleScrollNotification, child: PageView( scrollDirection: widget.scrollDirection, dragStartBehavior: widget.dragStartBehavior, clipBehavior: widget.clipBehavior, controller: _pageController, physics: widget.physics == null ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) : const PageScrollPhysics().applyTo(widget.physics), children: _childrenWithKey, ), ); } } ================================================ FILE: lib/common/widgets/flutter/text/paragraph.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'package:flutter/widgets.dart'; /// /// @docImport 'editable.dart'; library; import 'dart:math' as math; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, LineMetrics, Shader, TextBox, TextHeightBehavior; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' hide RenderParagraph; import 'package:flutter/services.dart'; /// The start and end positions for a text boundary. typedef _TextBoundaryRecord = ({ TextPosition boundaryStart, TextPosition boundaryEnd, }); /// Signature for a function that determines the [_TextBoundaryRecord] at the given /// [TextPosition]. typedef _TextBoundaryAtPosition = _TextBoundaryRecord Function(TextPosition position); /// Signature for a function that determines the [_TextBoundaryRecord] at the given /// [TextPosition], for the given [String]. typedef _TextBoundaryAtPositionInText = _TextBoundaryRecord Function(TextPosition position, String text); const String _kEllipsis = '\u2026'; class _UnspecifiedTextScaler extends TextScaler { const _UnspecifiedTextScaler(); @override Never get textScaleFactor => throw UnimplementedError(); @override Never scale(double fontSize) => throw UnimplementedError(); } /// A render object that displays a paragraph of text. class RenderParagraph extends RenderBox with ContainerRenderObjectMixin, RenderInlineChildrenContainerDefaults, RelayoutWhenSystemFontsChangeMixin { /// Creates a paragraph render object. /// /// The [maxLines] property may be null (and indeed defaults to null), but if /// it is not null, it must be greater than zero. RenderParagraph( InlineSpan text, { TextAlign textAlign = TextAlign.start, required TextDirection textDirection, bool softWrap = true, TextOverflow overflow = TextOverflow.clip, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) double textScaleFactor = 1.0, TextScaler textScaler = const _UnspecifiedTextScaler(), int? maxLines, Locale? locale, StrutStyle? strutStyle, TextWidthBasis textWidthBasis = TextWidthBasis.parent, ui.TextHeightBehavior? textHeightBehavior, List? children, Color? selectionColor, SelectionRegistrar? registrar, required Color primary, VoidCallback? onShowMore, }) : assert(text.debugAssertIsValid()), assert(maxLines == null || maxLines > 0), assert( identical(textScaler, const _UnspecifiedTextScaler()) || textScaleFactor == 1.0, 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', ), _primary = primary, _onShowMore = onShowMore, _softWrap = softWrap, _overflow = overflow, _selectionColor = selectionColor, _textPainter = TextPainter( text: text, textAlign: textAlign, textDirection: textDirection, textScaler: textScaler == const _UnspecifiedTextScaler() ? TextScaler.linear(textScaleFactor) : textScaler, maxLines: maxLines, ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, locale: locale, strutStyle: strutStyle, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, ) { addAll(children); this.registrar = registrar; } static final String _placeholderCharacter = String.fromCharCode( PlaceholderSpan.placeholderCodeUnit, ); final TextPainter _textPainter; // Currently, computing min/max intrinsic width/height will destroy state // inside the painter. Instead of calling _layout again to get back the correct // state, use a separate TextPainter for intrinsics calculation. // // TODO(abarth): Make computing the min/max intrinsic width/height a // non-destructive operation. TextPainter? _textIntrinsicsCache; TextPainter get _textIntrinsics { return (_textIntrinsicsCache ??= TextPainter()) ..text = _textPainter.text ..textAlign = _textPainter.textAlign ..textDirection = _textPainter.textDirection ..textScaler = _textPainter.textScaler ..maxLines = _textPainter.maxLines ..ellipsis = _textPainter.ellipsis ..locale = _textPainter.locale ..strutStyle = _textPainter.strutStyle ..textWidthBasis = _textPainter.textWidthBasis ..textHeightBehavior = _textPainter.textHeightBehavior; } List? _cachedAttributedLabels; List? _cachedCombinedSemanticsInfos; /// The text to display. InlineSpan get text => _textPainter.text!; set text(({InlineSpan text, Color primary}) params) { final value = params.text; _primary = params.primary; if (_morePainter case final textPainter?) { final textSpan = _moreTextSpan(value.style); switch (textPainter.text!.compareTo(textSpan)) { case RenderComparison.paint: textPainter.text = textSpan; case RenderComparison.layout: textPainter ..text = textSpan ..layout(); default: } } switch (_textPainter.text!.compareTo(value)) { case RenderComparison.identical: return; case RenderComparison.metadata: _textPainter.text = value; _cachedCombinedSemanticsInfos = null; markNeedsSemanticsUpdate(); case RenderComparison.paint: _textPainter.text = value; _cachedAttributedLabels = null; _cachedCombinedSemanticsInfos = null; markNeedsPaint(); markNeedsSemanticsUpdate(); case RenderComparison.layout: _textPainter.text = value; _overflowShader = null; _cachedAttributedLabels = null; _cachedCombinedSemanticsInfos = null; markNeedsLayout(); _removeSelectionRegistrarSubscription(); _disposeSelectableFragments(); _updateSelectionRegistrarSubscription(); } } /// The ongoing selections in this paragraph. /// /// The selection does not include selections in [PlaceholderSpan] if there /// are any. @visibleForTesting List get selections { if (_lastSelectableFragments == null) { return const []; } final List results = []; for (final _SelectableFragment fragment in _lastSelectableFragments!) { if (fragment._textSelectionStart != null && fragment._textSelectionEnd != null) { results.add( TextSelection( baseOffset: fragment._textSelectionStart!.offset, extentOffset: fragment._textSelectionEnd!.offset, ), ); } } return results; } // Should be null if selection is not enabled, i.e. _registrar = null. The // paragraph splits on [PlaceholderSpan.placeholderCodeUnit], and stores each // fragment in this list. List<_SelectableFragment>? _lastSelectableFragments; /// The [SelectionRegistrar] this paragraph will be, or is, registered to. SelectionRegistrar? get registrar => _registrar; SelectionRegistrar? _registrar; set registrar(SelectionRegistrar? value) { if (value == _registrar) { return; } _removeSelectionRegistrarSubscription(); _disposeSelectableFragments(); _registrar = value; _updateSelectionRegistrarSubscription(); } void _updateSelectionRegistrarSubscription() { if (_registrar == null) { return; } _lastSelectableFragments ??= _getSelectableFragments(); _lastSelectableFragments!.forEach(_registrar!.add); if (_lastSelectableFragments!.isNotEmpty) { markNeedsCompositingBitsUpdate(); } } void _removeSelectionRegistrarSubscription() { if (_registrar == null || _lastSelectableFragments == null) { return; } _lastSelectableFragments!.forEach(_registrar!.remove); } List<_SelectableFragment> _getSelectableFragments() { final String plainText = text.toPlainText(includeSemanticsLabels: false); final List<_SelectableFragment> result = <_SelectableFragment>[]; int start = 0; while (start < plainText.length) { int end = plainText.indexOf(_placeholderCharacter, start); if (start != end) { if (end == -1) { end = plainText.length; } result.add( _SelectableFragment( paragraph: this, range: TextRange(start: start, end: end), fullText: plainText, ), ); start = end; } start += 1; } return result; } /// Determines whether the given [Selectable] was created by this /// [RenderParagraph]. /// /// The [RenderParagraph] splits its text into multiple [Selectable]s, /// delimited by [PlaceholderSpan]s or [WidgetSpan]s. bool selectableBelongsToParagraph(Selectable selectable) { if (_lastSelectableFragments == null) { return false; } return _lastSelectableFragments!.contains(selectable); } void _disposeSelectableFragments() { if (_lastSelectableFragments == null) { return; } for (final _SelectableFragment fragment in _lastSelectableFragments!) { fragment.dispose(); } _lastSelectableFragments = null; } @override bool get alwaysNeedsCompositing => _lastSelectableFragments?.isNotEmpty ?? false; @override void markNeedsLayout() { _lastSelectableFragments?.forEach( (_SelectableFragment element) => element.didChangeParagraphLayout(), ); super.markNeedsLayout(); } @override void dispose() { _removeSelectionRegistrarSubscription(); _disposeSelectableFragments(); _textPainter.dispose(); _textIntrinsicsCache?.dispose(); _tapGestureRecognizer?.dispose(); _tapGestureRecognizer = null; _morePainter?.dispose(); _morePainter = null; super.dispose(); } /// How the text should be aligned horizontally. TextAlign get textAlign => _textPainter.textAlign; set textAlign(TextAlign value) { if (_textPainter.textAlign == value) { return; } _textPainter.textAlign = value; markNeedsPaint(); } /// The directionality of the text. /// /// This decides how the [TextAlign.start], [TextAlign.end], and /// [TextAlign.justify] values of [textAlign] are interpreted. /// /// This is also used to disambiguate how to render bidirectional text. For /// example, if the [text] is an English phrase followed by a Hebrew phrase, /// in a [TextDirection.ltr] context the English phrase will be on the left /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. TextDirection get textDirection => _textPainter.textDirection!; set textDirection(TextDirection value) { if (_textPainter.textDirection == value) { return; } _textPainter.textDirection = value; markNeedsLayout(); } /// Whether the text should break at soft line breaks. /// /// If false, the glyphs in the text will be positioned as if there was /// unlimited horizontal space. /// /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected /// effects. bool get softWrap => _softWrap; bool _softWrap; set softWrap(bool value) { if (_softWrap == value) { return; } _softWrap = value; markNeedsLayout(); } /// How visual overflow should be handled. TextOverflow get overflow => _overflow; TextOverflow _overflow; set overflow(TextOverflow value) { if (_overflow == value) { return; } _overflow = value; _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null; markNeedsLayout(); } /// Deprecated. Will be removed in a future version of Flutter. Use /// [textScaler] instead. /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) double get textScaleFactor => _textPainter.textScaleFactor; @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) set textScaleFactor(double value) { textScaler = TextScaler.linear(value); } /// {@macro flutter.painting.textPainter.textScaler} TextScaler get textScaler => _textPainter.textScaler; set textScaler(TextScaler value) { if (_textPainter.textScaler == value) { return; } _morePainter ?..textScaler = value ..layout(); _textPainter.textScaler = value; _overflowShader = null; markNeedsLayout(); } /// An optional maximum number of lines for the text to span, wrapping if /// necessary. If the text exceeds the given number of lines, it will be /// truncated according to [overflow] and [softWrap]. int? get maxLines => _textPainter.maxLines; /// The value may be null. If it is not null, then it must be greater than /// zero. set maxLines(int? value) { assert(value == null || value > 0); if (_textPainter.maxLines == value) { return; } _textPainter.maxLines = value; _overflowShader = null; markNeedsLayout(); } /// Used by this paragraph's internal [TextPainter] to select a /// locale-specific font. /// /// In some cases, the same Unicode character may be rendered differently /// depending on the locale. For example, the '骨' character is rendered /// differently in the Chinese and Japanese locales. In these cases, the /// [locale] may be used to select a locale-specific font. Locale? get locale => _textPainter.locale; /// The value may be null. set locale(Locale? value) { if (_textPainter.locale == value) { return; } _textPainter.locale = value; _overflowShader = null; markNeedsLayout(); } /// {@macro flutter.painting.textPainter.strutStyle} StrutStyle? get strutStyle => _textPainter.strutStyle; /// The value may be null. set strutStyle(StrutStyle? value) { if (_textPainter.strutStyle == value) { return; } _textPainter.strutStyle = value; _overflowShader = null; markNeedsLayout(); } /// {@macro flutter.painting.textPainter.textWidthBasis} TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis; set textWidthBasis(TextWidthBasis value) { if (_textPainter.textWidthBasis == value) { return; } _textPainter.textWidthBasis = value; _overflowShader = null; markNeedsLayout(); } /// {@macro dart.ui.textHeightBehavior} ui.TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior; set textHeightBehavior(ui.TextHeightBehavior? value) { if (_textPainter.textHeightBehavior == value) { return; } _textPainter.textHeightBehavior = value; _overflowShader = null; markNeedsLayout(); } /// The color to use when painting the selection. /// /// Ignored if the text is not selectable (e.g. if [registrar] is null). Color? get selectionColor => _selectionColor; Color? _selectionColor; set selectionColor(Color? value) { if (_selectionColor == value) { return; } _selectionColor = value; if (_lastSelectableFragments?.any( (_SelectableFragment fragment) => fragment.value.hasSelection, ) ?? false) { markNeedsPaint(); } } Offset _getOffsetForPosition(TextPosition position) { return getOffsetForCaret(position, Rect.zero) + Offset(0, getFullHeightForCaret(position)); } @override double computeMinIntrinsicWidth(double height) { final List placeholderDimensions = layoutInlineChildren( double.infinity, (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0), ChildLayoutHelper.getDryBaseline, ); return (_textIntrinsics ..setPlaceholderDimensions(placeholderDimensions) ..layout()) .minIntrinsicWidth; } @override double computeMaxIntrinsicWidth(double height) { final List placeholderDimensions = layoutInlineChildren( double.infinity, // Height and baseline is irrelevant as all text will be laid // out in a single line. Therefore, using 0.0 as a dummy for the height. (RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0), ChildLayoutHelper.getDryBaseline, ); return (_textIntrinsics ..setPlaceholderDimensions(placeholderDimensions) ..layout()) .maxIntrinsicWidth; } /// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight]. /// /// This does not require the layout to be updated. @visibleForTesting double get preferredLineHeight => _textPainter.preferredLineHeight; double _computeIntrinsicHeight(double width) { return (_textIntrinsics ..setPlaceholderDimensions( layoutInlineChildren( width, ChildLayoutHelper.dryLayoutChild, ChildLayoutHelper.getDryBaseline, ), ) ..layout(minWidth: width, maxWidth: _adjustMaxWidth(width))) .height; } @override double computeMinIntrinsicHeight(double width) { return _computeIntrinsicHeight(width); } @override double computeMaxIntrinsicHeight(double width) { return _computeIntrinsicHeight(width); } @override bool hitTestSelf(Offset position) => true; @override @protected bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { if (_tapGestureRecognizer != null) { if (_morePainter case final textPainter?) { late final height = _textPainter.height; if (position.dx < textPainter.width && position.dy > height && position.dy < height + textPainter.height) { result.add(HitTestEntry(textPainter.text as TextSpan)); return true; } } } final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position); // The hit-test can't fall through the horizontal gaps between visually // adjacent characters on the same line, even with a large letter-spacing or // text justification, as graphemeClusterLayoutBounds.width is the advance // width to the next character, so there's no gap between their // graphemeClusterLayoutBounds rects. final InlineSpan? spanHit = glyph != null && glyph.graphemeClusterLayoutBounds.contains(position) ? _textPainter.text!.getSpanForPosition( TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start), ) : null; switch (spanHit) { case final HitTestTarget span: result.add(HitTestEntry(span)); return true; case _: return hitTestInlineChildren(result, position); } } bool _needsClipping = false; ui.Shader? _overflowShader; /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow /// effect. /// /// Used to test this object. Not for use in production. @visibleForTesting bool get debugHasOverflowShader => _overflowShader != null; @override void systemFontsDidChange() { super.systemFontsDidChange(); _textPainter.markNeedsLayout(); } // Placeholder dimensions representing the sizes of child inline widgets. // // These need to be cached because the text painter's placeholder dimensions // will be overwritten during intrinsic width/height calculations and must be // restored to the original values before final layout and painting. List? _placeholderDimensions; double _adjustMaxWidth(double maxWidth) { return softWrap || overflow == TextOverflow.ellipsis ? maxWidth : double.infinity; } void _layoutTextWithConstraints(BoxConstraints constraints) { _textPainter ..setPlaceholderDimensions(_placeholderDimensions) ..layout( minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth), ); } @override @protected Size computeDryLayout(covariant BoxConstraints constraints) { final Size size = (_textIntrinsics ..setPlaceholderDimensions( layoutInlineChildren( constraints.maxWidth, ChildLayoutHelper.dryLayoutChild, ChildLayoutHelper.getDryBaseline, ), ) ..layout( minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth), )) .size; return constraints.constrain(size); } @override double computeDistanceToActualBaseline(TextBaseline baseline) { assert(!debugNeedsLayout); assert(constraints.debugAssertIsValid()); _layoutTextWithConstraints(constraints); // TODO(garyq): Since our metric for ideographic baseline is currently // inaccurate and the non-alphabetic baselines are based off of the // alphabetic baseline, we use the alphabetic for now to produce correct // layouts. We should eventually change this back to pass the `baseline` // property when the ideographic baseline is properly implemented // (https://github.com/flutter/flutter/issues/22625). return _textPainter.computeDistanceToActualBaseline( TextBaseline.alphabetic, ); } @override double computeDryBaseline( covariant BoxConstraints constraints, TextBaseline baseline, ) { assert(constraints.debugAssertIsValid()); _textIntrinsics ..setPlaceholderDimensions( layoutInlineChildren( constraints.maxWidth, ChildLayoutHelper.dryLayoutChild, ChildLayoutHelper.getDryBaseline, ), ) ..layout( minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth), ); return _textIntrinsics.computeDistanceToActualBaseline( TextBaseline.alphabetic, ); } Color _primary; VoidCallback? _onShowMore; set onShowMore(VoidCallback? onShowMore) { if (_onShowMore != onShowMore) { _onShowMore = onShowMore; _tapGestureRecognizer?.onTap = onShowMore; } } TapGestureRecognizer? _tapGestureRecognizer; TextSpan _moreTextSpan([TextStyle? style]) => TextSpan( style: (style ?? text.style!).copyWith(color: _primary), text: '查看更多', recognizer: _tapGestureRecognizer, ); TextPainter? _morePainter; bool didOverflowHeight = false; @override void performLayout() { _lastSelectableFragments?.forEach( (_SelectableFragment element) => element.didChangeParagraphLayout(), ); final BoxConstraints constraints = this.constraints; _placeholderDimensions = layoutInlineChildren( constraints.maxWidth, ChildLayoutHelper.layoutChild, ChildLayoutHelper.getBaseline, ); _layoutTextWithConstraints(constraints); positionInlineChildren(_textPainter.inlinePlaceholderBoxes!); final Size textSize = _textPainter.size; size = constraints.constrain(textSize); didOverflowHeight = size.height < textSize.height || _textPainter.didExceedMaxLines; if (didOverflowHeight) { if (_onShowMore != null) { _tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore; } _morePainter ??= TextPainter( text: _moreTextSpan(), textDirection: textDirection, textScaler: textScaler, locale: locale, )..layout(maxWidth: constraints.maxWidth); size = Size( size.width, constraints.constrainHeight(size.height + _morePainter!.height), ); } final bool didOverflowWidth = size.width < textSize.width; // TODO(abarth): We're only measuring the sizes of the line boxes here. If // the glyphs draw outside the line boxes, we might think that there isn't // visual overflow when there actually is visual overflow. This can become // a problem if we start having horizontal overflow and introduce a clip // that affects the actual (but undetected) vertical overflow. final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight; if (hasVisualOverflow) { switch (_overflow) { case TextOverflow.visible: _needsClipping = false; _overflowShader = null; case TextOverflow.clip: case TextOverflow.ellipsis: _needsClipping = true; _overflowShader = null; case TextOverflow.fade: _needsClipping = true; final TextPainter fadeSizePainter = TextPainter( text: TextSpan(style: _textPainter.text!.style, text: '\u2026'), textDirection: textDirection, textScaler: textScaler, locale: locale, )..layout(); if (didOverflowWidth) { final (double fadeStart, double fadeEnd) = switch (textDirection) { TextDirection.rtl => (fadeSizePainter.width, 0.0), TextDirection.ltr => ( size.width - fadeSizePainter.width, size.width, ), }; _overflowShader = ui.Gradient.linear( Offset(fadeStart, 0.0), Offset(fadeEnd, 0.0), [const Color(0xFFFFFFFF), const Color(0x00FFFFFF)], ); } else { final double fadeEnd = size.height; final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0; _overflowShader = ui.Gradient.linear( Offset(0.0, fadeStart), Offset(0.0, fadeEnd), [const Color(0xFFFFFFFF), const Color(0x00FFFFFF)], ); } fadeSizePainter.dispose(); } } else { _needsClipping = false; _overflowShader = null; } } @override void applyPaintTransform(RenderBox child, Matrix4 transform) { defaultApplyPaintTransform(child, transform); } @override void paint(PaintingContext context, Offset offset) { // Text alignment only triggers repaint so it's possible the text layout has // been invalidated but performLayout wasn't called at this point. Make sure // the TextPainter has a valid layout. _layoutTextWithConstraints(constraints); assert(() { if (debugRepaintTextRainbowEnabled) { final Paint paint = Paint()..color = debugCurrentRepaintColor.toColor(); context.canvas.drawRect(offset & size, paint); } return true; }()); if (_needsClipping) { final Rect bounds = offset & size; if (_overflowShader != null) { // This layer limits what the shader below blends with to be just the // text (as opposed to the text and its background). context.canvas.saveLayer(bounds, Paint()); } else { context.canvas.save(); } context.canvas.clipRect(bounds); } if (_lastSelectableFragments != null) { for (final _SelectableFragment fragment in _lastSelectableFragments!) { fragment.paint(context, offset); } } assert(() { _textPainter.debugPaintTextLayoutBoxes = debugPaintTextLayoutBoxes; return true; }()); _textPainter.paint(context.canvas, offset); paintInlineChildren(context, offset); if (_needsClipping) { if (_overflowShader != null) { context.canvas.translate(offset.dx, offset.dy); final Paint paint = Paint() ..blendMode = BlendMode.modulate ..shader = _overflowShader; context.canvas.drawRect(Offset.zero & size, paint); } context.canvas.restore(); } if (didOverflowHeight) { _morePainter?.paint( context.canvas, offset + Offset(0, _textPainter.height), ); } } /// Returns the offset at which to paint the caret. /// /// Valid only after [layout]. Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { assert(!debugNeedsLayout); _layoutTextWithConstraints(constraints); return _textPainter.getOffsetForCaret(position, caretPrototype); } /// {@macro flutter.painting.textPainter.getFullHeightForCaret} /// /// Valid only after [layout]. double getFullHeightForCaret(TextPosition position) { assert(!debugNeedsLayout); _layoutTextWithConstraints(constraints); return _textPainter.getFullHeightForCaret(position, Rect.zero); } /// Returns a list of rects that bound the given selection. /// /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select /// the shape of the [TextBox]es. These properties default to /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively. /// /// A given selection might have more than one rect if the [RenderParagraph] /// contains multiple [InlineSpan]s or bidirectional text, because logically /// contiguous text might not be visually contiguous. /// /// Valid only after [layout]. /// /// See also: /// /// * [TextPainter.getBoxesForSelection], the method in TextPainter to get /// the equivalent boxes. List getBoxesForSelection( TextSelection selection, { ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight, }) { assert(!debugNeedsLayout); _layoutTextWithConstraints(constraints); return _textPainter.getBoxesForSelection( selection, boxHeightStyle: boxHeightStyle, boxWidthStyle: boxWidthStyle, ); } /// Returns the position within the text for the given pixel offset. /// /// Valid only after [layout]. TextPosition getPositionForOffset(Offset offset) { assert(!debugNeedsLayout); _layoutTextWithConstraints(constraints); return _textPainter.getPositionForOffset(offset); } /// Returns the text range of the word at the given offset. Characters not /// part of a word, such as spaces, symbols, and punctuation, have word breaks /// on both sides. In such cases, this method will return a text range that /// contains the given text position. /// /// Word boundaries are defined more precisely in Unicode Standard Annex #29 /// . /// /// Valid only after [layout]. TextRange getWordBoundary(TextPosition position) { assert(!debugNeedsLayout); _layoutTextWithConstraints(constraints); return _textPainter.getWordBoundary(position); } TextRange _getLineAtOffset(TextPosition position) => _textPainter.getLineBoundary(position); TextPosition _getTextPositionAbove(TextPosition position) { // -0.5 of preferredLineHeight points to the middle of the line above. final double preferredLineHeight = _textPainter.preferredLineHeight; final double verticalOffset = -0.5 * preferredLineHeight; return _getTextPositionVertical(position, verticalOffset); } TextPosition _getTextPositionBelow(TextPosition position) { // 1.5 of preferredLineHeight points to the middle of the line below. final double preferredLineHeight = _textPainter.preferredLineHeight; final double verticalOffset = 1.5 * preferredLineHeight; return _getTextPositionVertical(position, verticalOffset); } TextPosition _getTextPositionVertical( TextPosition position, double verticalOffset, ) { final Offset caretOffset = _textPainter.getOffsetForCaret( position, Rect.zero, ); final Offset caretOffsetTranslated = caretOffset.translate( 0.0, verticalOffset, ); return _textPainter.getPositionForOffset(caretOffsetTranslated); } /// Returns the size of the text as laid out. /// /// This can differ from [size] if the text overflowed or if the [constraints] /// provided by the parent [RenderObject] forced the layout to be bigger than /// necessary for the given [text]. /// /// This returns the [TextPainter.size] of the underlying [TextPainter]. /// /// Valid only after [layout]. Size get textSize { assert(!debugNeedsLayout); return _textPainter.size; } /// Whether the text was truncated or ellipsized as laid out. /// /// This returns the [TextPainter.didExceedMaxLines] of the underlying [TextPainter]. /// /// Valid only after [layout]. bool get didExceedMaxLines { assert(!debugNeedsLayout); return _textPainter.didExceedMaxLines; } /// Collected during [describeSemanticsConfiguration], used by /// [assembleSemanticsNode]. List? _semanticsInfo; @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); _semanticsInfo = text.getSemanticsInformation(); bool needsAssembleSemanticsNode = false; bool needsChildConfigurationsDelegate = false; for (final InlineSpanSemanticsInformation info in _semanticsInfo!) { if (info.recognizer != null || info.semanticsIdentifier != null) { needsAssembleSemanticsNode = true; break; } needsChildConfigurationsDelegate = needsChildConfigurationsDelegate || info.isPlaceholder; } if (needsAssembleSemanticsNode) { config ..explicitChildNodes = true ..isSemanticBoundary = true; } else if (needsChildConfigurationsDelegate) { config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate; } else { if (_cachedAttributedLabels == null) { final StringBuffer buffer = StringBuffer(); int offset = 0; final List attributes = []; for (final InlineSpanSemanticsInformation info in _semanticsInfo!) { final String label = info.semanticsLabel ?? info.text; for (final StringAttribute infoAttribute in info.stringAttributes) { final TextRange originalRange = infoAttribute.range; attributes.add( infoAttribute.copy( range: TextRange( start: offset + originalRange.start, end: offset + originalRange.end, ), ), ); } buffer.write(label); offset += label.length; } _cachedAttributedLabels = [ AttributedString(buffer.toString(), attributes: attributes), ]; } config ..attributedLabel = _cachedAttributedLabels![0] ..textDirection = textDirection; } } ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate( List childConfigs, ) { final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder(); int placeholderIndex = 0; int childConfigsIndex = 0; int attributedLabelCacheIndex = 0; InlineSpanSemanticsInformation? seenTextInfo; _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) { if (info.isPlaceholder) { if (seenTextInfo != null) { builder.markAsMergeUp( _createSemanticsConfigForTextInfo( seenTextInfo, attributedLabelCacheIndex, ), ); attributedLabelCacheIndex += 1; } // Mark every childConfig belongs to this placeholder to merge up group. while (childConfigsIndex < childConfigs.length && childConfigs[childConfigsIndex].tagsChildrenWith( PlaceholderSpanIndexSemanticsTag(placeholderIndex), )) { builder.markAsMergeUp(childConfigs[childConfigsIndex]); childConfigsIndex += 1; } placeholderIndex += 1; } else { seenTextInfo = info; } } // Handle plain text info at the end. if (seenTextInfo != null) { builder.markAsMergeUp( _createSemanticsConfigForTextInfo( seenTextInfo, attributedLabelCacheIndex, ), ); } return builder.build(); } SemanticsConfiguration _createSemanticsConfigForTextInfo( InlineSpanSemanticsInformation textInfo, int cacheIndex, ) { assert(!textInfo.requiresOwnNode); final List cachedStrings = _cachedAttributedLabels ??= []; assert(cacheIndex <= cachedStrings.length); final bool hasCache = cacheIndex < cachedStrings.length; late AttributedString attributedLabel; if (hasCache) { attributedLabel = cachedStrings[cacheIndex]; } else { assert(cachedStrings.length == cacheIndex); attributedLabel = AttributedString( textInfo.semanticsLabel ?? textInfo.text, attributes: textInfo.stringAttributes, ); cachedStrings.add(attributedLabel); } return SemanticsConfiguration() ..textDirection = textDirection ..attributedLabel = attributedLabel; } // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they // can be re-used when [assembleSemanticsNode] is called again. This ensures // stable ids for the [SemanticsNode]s of [TextSpan]s across // [assembleSemanticsNode] invocations. Map? _cachedChildNodes; @override void assembleSemanticsNode( SemanticsNode node, SemanticsConfiguration config, Iterable children, ) { assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty); final List newChildren = []; TextDirection currentDirection = textDirection; Rect currentRect; double ordinal = 0.0; int start = 0; int placeholderIndex = 0; int childIndex = 0; RenderBox? child = firstChild; final Map newChildCache = {}; _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) { final TextSelection selection = TextSelection( baseOffset: start, extentOffset: start + info.text.length, ); start += info.text.length; if (info.isPlaceholder) { // A placeholder span may have 0 to multiple semantics nodes, we need // to annotate all of the semantics nodes belong to this span. while (children.length > childIndex && children .elementAt(childIndex) .isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) { final SemanticsNode childNode = children.elementAt(childIndex); final TextParentData parentData = child!.parentData! as TextParentData; // parentData.scale may be null if the render object is truncated. if (parentData.offset != null) { newChildren.add(childNode); } childIndex += 1; } child = childAfter(child!); placeholderIndex += 1; } else { final TextDirection initialDirection = currentDirection; final List rects = getBoxesForSelection(selection); if (rects.isEmpty) { continue; } Rect rect = rects.first.toRect(); currentDirection = rects.first.direction; for (final ui.TextBox textBox in rects.skip(1)) { rect = rect.expandToInclude(textBox.toRect()); currentDirection = textBox.direction; } // Any of the text boxes may have had infinite dimensions. // We shouldn't pass infinite dimensions up to the bridges. rect = Rect.fromLTWH( math.max(0.0, rect.left), math.max(0.0, rect.top), math.min(rect.width, constraints.maxWidth), math.min(rect.height, constraints.maxHeight), ); // round the current rectangle to make this API testable and add some // padding so that the accessibility rects do not overlap with the text. currentRect = Rect.fromLTRB( rect.left.floorToDouble() - 4.0, rect.top.floorToDouble() - 4.0, rect.right.ceilToDouble() + 4.0, rect.bottom.ceilToDouble() + 4.0, ); final SemanticsConfiguration configuration = SemanticsConfiguration() ..sortKey = OrdinalSortKey(ordinal++) ..textDirection = initialDirection ..identifier = info.semanticsIdentifier ?? '' ..attributedLabel = AttributedString( info.semanticsLabel ?? info.text, attributes: info.stringAttributes, ); switch (info.recognizer) { case TapGestureRecognizer(onTap: final VoidCallback? handler): case DoubleTapGestureRecognizer( onDoubleTap: final VoidCallback? handler, ): if (handler != null) { configuration ..onTap = handler ..isLink = true; } case LongPressGestureRecognizer( onLongPress: final GestureLongPressCallback? onLongPress, ): if (onLongPress != null) { configuration.onLongPress = onLongPress; } case null: break; default: assert(false, '${info.recognizer.runtimeType} is not supported.'); } if (node.parentPaintClipRect != null) { final Rect paintRect = node.parentPaintClipRect!.intersect( currentRect, ); configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty; } final SemanticsNode newChild; if (_cachedChildNodes?.isNotEmpty ?? false) { newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!; } else { final UniqueKey key = UniqueKey(); newChild = SemanticsNode( key: key, showOnScreen: _createShowOnScreenFor(key), ); } newChild ..updateWith(config: configuration) ..rect = currentRect; newChildCache[newChild.key!] = newChild; newChildren.add(newChild); } } // Makes sure we annotated all of the semantics children. assert(childIndex == children.length); assert(child == null); _cachedChildNodes = newChildCache; node.updateWith(config: config, childrenInInversePaintOrder: newChildren); } VoidCallback? _createShowOnScreenFor(Key key) { return () { final SemanticsNode node = _cachedChildNodes![key]!; showOnScreen(descendant: this, rect: node.rect); }; } @override void clearSemantics() { super.clearSemantics(); _cachedChildNodes = null; } @override List debugDescribeChildren() { return [ text.toDiagnosticsNode( name: 'text', style: DiagnosticsTreeStyle.transition, ), ]; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(EnumProperty('textAlign', textAlign)) ..add(EnumProperty('textDirection', textDirection)) ..add( FlagProperty( 'softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true, ), ) ..add(EnumProperty('overflow', overflow)) ..add( DiagnosticsProperty( 'textScaler', textScaler, defaultValue: TextScaler.noScaling, ), ) ..add( DiagnosticsProperty('locale', locale, defaultValue: null), ) ..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); } } /// A continuous, selectable piece of paragraph. /// /// Since the selections in [PlaceholderSpan] are handled independently in its /// subtree, a selection in [RenderParagraph] can't continue across a /// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan] /// to create multiple `_SelectableFragment`s so that they can be selected /// separately. class _SelectableFragment with Selectable, Diagnosticable, ChangeNotifier implements TextLayoutMetrics { _SelectableFragment({ required this.paragraph, required this.fullText, required this.range, }) : assert(range.isValid && !range.isCollapsed && range.isNormalized) { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } _selectionGeometry = _getSelectionGeometry(); } final TextRange range; final RenderParagraph paragraph; final String fullText; TextPosition? _textSelectionStart; TextPosition? _textSelectionEnd; bool _selectableContainsOriginTextBoundary = false; LayerLink? _startHandleLayerLink; LayerLink? _endHandleLayerLink; @override SelectionGeometry get value => _selectionGeometry; late SelectionGeometry _selectionGeometry; void _updateSelectionGeometry() { final SelectionGeometry newValue = _getSelectionGeometry(); if (_selectionGeometry == newValue) { return; } _selectionGeometry = newValue; notifyListeners(); } SelectionGeometry _getSelectionGeometry() { if (_textSelectionStart == null || _textSelectionEnd == null) { return const SelectionGeometry( status: SelectionStatus.none, hasContent: true, ); } final int selectionStart = _textSelectionStart!.offset; final int selectionEnd = _textSelectionEnd!.offset; final bool isReversed = selectionStart > selectionEnd; final Offset startOffsetInParagraphCoordinates = paragraph ._getOffsetForPosition( TextPosition(offset: selectionStart), ); final Offset endOffsetInParagraphCoordinates = selectionStart == selectionEnd ? startOffsetInParagraphCoordinates : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd)); final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection); final TextSelection selection = TextSelection( baseOffset: selectionStart, extentOffset: selectionEnd, ); final List selectionRects = []; for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) { selectionRects.add(textBox.toRect()); } final bool selectionCollapsed = selectionStart == selectionEnd; final ( TextSelectionHandleType startSelectionHandleType, TextSelectionHandleType endSelectionHandleType, ) = switch ((selectionCollapsed, flipHandles)) { // Always prefer collapsed handle when selection is collapsed. (true, _) => ( TextSelectionHandleType.collapsed, TextSelectionHandleType.collapsed, ), (false, true) => ( TextSelectionHandleType.right, TextSelectionHandleType.left, ), (false, false) => ( TextSelectionHandleType.left, TextSelectionHandleType.right, ), }; return SelectionGeometry( startSelectionPoint: SelectionPoint( localPosition: startOffsetInParagraphCoordinates, lineHeight: paragraph._textPainter.preferredLineHeight, handleType: startSelectionHandleType, ), endSelectionPoint: SelectionPoint( localPosition: endOffsetInParagraphCoordinates, lineHeight: paragraph._textPainter.preferredLineHeight, handleType: endSelectionHandleType, ), selectionRects: selectionRects, status: selectionCollapsed ? SelectionStatus.collapsed : SelectionStatus.uncollapsed, hasContent: true, ); } @override SelectionResult dispatchSelectionEvent(SelectionEvent event) { late final SelectionResult result; final TextPosition? existingSelectionStart = _textSelectionStart; final TextPosition? existingSelectionEnd = _textSelectionEnd; switch (event.type) { case SelectionEventType.startEdgeUpdate: case SelectionEventType.endEdgeUpdate: final SelectionEdgeUpdateEvent edgeUpdate = event as SelectionEdgeUpdateEvent; final TextGranularity granularity = event.granularity; switch (granularity) { case TextGranularity.character: result = _updateSelectionEdge( edgeUpdate.globalPosition, isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate, ); case TextGranularity.word: result = _updateSelectionEdgeByTextBoundary( edgeUpdate.globalPosition, isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate, getTextBoundary: _getWordBoundaryAtPosition, ); case TextGranularity.paragraph: result = _updateSelectionEdgeByMultiSelectableTextBoundary( edgeUpdate.globalPosition, isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate, getTextBoundary: _getParagraphBoundaryAtPosition, getClampedTextBoundary: _getClampedParagraphBoundaryAtPosition, ); case TextGranularity.document: case TextGranularity.line: assert( false, 'Moving the selection edge by line or document is not supported.', ); } case SelectionEventType.clear: result = _handleClearSelection(); case SelectionEventType.selectAll: result = _handleSelectAll(); case SelectionEventType.selectWord: final SelectWordSelectionEvent selectWord = event as SelectWordSelectionEvent; result = _handleSelectWord(selectWord.globalPosition); case SelectionEventType.selectParagraph: final SelectParagraphSelectionEvent selectParagraph = event as SelectParagraphSelectionEvent; if (selectParagraph.absorb) { _handleSelectAll(); result = SelectionResult.next; _selectableContainsOriginTextBoundary = true; } else { result = _handleSelectParagraph(selectParagraph.globalPosition); } case SelectionEventType.granularlyExtendSelection: final GranularlyExtendSelectionEvent granularlyExtendSelection = event as GranularlyExtendSelectionEvent; result = _handleGranularlyExtendSelection( granularlyExtendSelection.forward, granularlyExtendSelection.isEnd, granularlyExtendSelection.granularity, ); case SelectionEventType.directionallyExtendSelection: final DirectionallyExtendSelectionEvent directionallyExtendSelection = event as DirectionallyExtendSelectionEvent; result = _handleDirectionallyExtendSelection( directionallyExtendSelection.dx, directionallyExtendSelection.isEnd, directionallyExtendSelection.direction, ); } if (existingSelectionStart != _textSelectionStart || existingSelectionEnd != _textSelectionEnd) { _didChangeSelection(); } return result; } @override SelectedContent? getSelectedContent() { if (_textSelectionStart == null || _textSelectionEnd == null) { return null; } final int start = math.min( _textSelectionStart!.offset, _textSelectionEnd!.offset, ); final int end = math.max( _textSelectionStart!.offset, _textSelectionEnd!.offset, ); return SelectedContent(plainText: fullText.substring(start, end)); } @override SelectedContentRange? getSelection() { if (_textSelectionStart == null || _textSelectionEnd == null) { return null; } return SelectedContentRange( startOffset: _textSelectionStart!.offset, endOffset: _textSelectionEnd!.offset, ); } void _didChangeSelection() { paragraph.markNeedsPaint(); _updateSelectionGeometry(); } TextPosition _updateSelectionStartEdgeByTextBoundary( _TextBoundaryRecord? textBoundary, _TextBoundaryAtPosition getTextBoundary, TextPosition position, TextPosition? existingSelectionStart, TextPosition? existingSelectionEnd, ) { TextPosition? targetPosition; if (textBoundary != null) { assert( textBoundary.boundaryStart.offset >= range.start && textBoundary.boundaryEnd.offset <= range.end, ); if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { final bool isSamePosition = position.offset == existingSelectionEnd.offset; final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset > existingSelectionEnd.offset)); if (shouldSwapEdges) { if (position.offset < existingSelectionEnd.offset) { targetPosition = textBoundary.boundaryStart; } else { targetPosition = textBoundary.boundaryEnd; } // When the selection is inverted by the new position it is necessary to // swap the start edge (moving edge) with the end edge (static edge) to // maintain the origin text boundary within the selection. final _TextBoundaryRecord localTextBoundary = getTextBoundary( existingSelectionEnd, ); assert( localTextBoundary.boundaryStart.offset >= range.start && localTextBoundary.boundaryEnd.offset <= range.end, ); _setSelectionPosition( existingSelectionEnd.offset == localTextBoundary.boundaryStart.offset ? localTextBoundary.boundaryEnd : localTextBoundary.boundaryStart, isEnd: true, ); } else { if (position.offset < existingSelectionEnd.offset) { targetPosition = textBoundary.boundaryStart; } else if (position.offset > existingSelectionEnd.offset) { targetPosition = textBoundary.boundaryEnd; } else { // Keep the origin text boundary in bounds when position is at the static edge. targetPosition = existingSelectionStart; } } } else { if (existingSelectionEnd != null) { // If the end edge exists and the start edge is being moved, then the // start edge is moved to encompass the entire text boundary at the new position. if (position.offset < existingSelectionEnd.offset) { targetPosition = textBoundary.boundaryStart; } else { targetPosition = textBoundary.boundaryEnd; } } else { // Move the start edge to the closest text boundary. targetPosition = _closestTextBoundary(textBoundary, position); } } } else { // The position is not contained within the current rect. The targetPosition // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset] // for a more in depth explanation on this adjustment. if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { // When the selection is inverted by the new position it is necessary to // swap the start edge (moving edge) with the end edge (static edge) to // maintain the origin text boundary within the selection. final bool isSamePosition = position.offset == existingSelectionEnd.offset; final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset > existingSelectionEnd.offset)); if (shouldSwapEdges) { final _TextBoundaryRecord localTextBoundary = getTextBoundary( existingSelectionEnd, ); assert( localTextBoundary.boundaryStart.offset >= range.start && localTextBoundary.boundaryEnd.offset <= range.end, ); _setSelectionPosition( isSelectionInverted ? localTextBoundary.boundaryEnd : localTextBoundary.boundaryStart, isEnd: true, ); } } } return targetPosition ?? position; } TextPosition _updateSelectionEndEdgeByTextBoundary( _TextBoundaryRecord? textBoundary, _TextBoundaryAtPosition getTextBoundary, TextPosition position, TextPosition? existingSelectionStart, TextPosition? existingSelectionEnd, ) { TextPosition? targetPosition; if (textBoundary != null) { assert( textBoundary.boundaryStart.offset >= range.start && textBoundary.boundaryEnd.offset <= range.end, ); if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { final bool isSamePosition = position.offset == existingSelectionStart.offset; final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset < existingSelectionStart.offset)); if (shouldSwapEdges) { if (position.offset < existingSelectionStart.offset) { targetPosition = textBoundary.boundaryStart; } else { targetPosition = textBoundary.boundaryEnd; } // When the selection is inverted by the new position it is necessary to // swap the end edge (moving edge) with the start edge (static edge) to // maintain the origin text boundary within the selection. final _TextBoundaryRecord localTextBoundary = getTextBoundary( existingSelectionStart, ); assert( localTextBoundary.boundaryStart.offset >= range.start && localTextBoundary.boundaryEnd.offset <= range.end, ); _setSelectionPosition( existingSelectionStart.offset == localTextBoundary.boundaryStart.offset ? localTextBoundary.boundaryEnd : localTextBoundary.boundaryStart, isEnd: false, ); } else { if (position.offset < existingSelectionStart.offset) { targetPosition = textBoundary.boundaryStart; } else if (position.offset > existingSelectionStart.offset) { targetPosition = textBoundary.boundaryEnd; } else { // Keep the origin text boundary in bounds when position is at the static edge. targetPosition = existingSelectionEnd; } } } else { if (existingSelectionStart != null) { // If the start edge exists and the end edge is being moved, then the // end edge is moved to encompass the entire text boundary at the new position. if (position.offset < existingSelectionStart.offset) { targetPosition = textBoundary.boundaryStart; } else { targetPosition = textBoundary.boundaryEnd; } } else { // Move the end edge to the closest text boundary. targetPosition = _closestTextBoundary(textBoundary, position); } } } else { // The position is not contained within the current rect. The targetPosition // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset] // for a more in depth explanation on this adjustment. if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { // When the selection is inverted by the new position it is necessary to // swap the end edge (moving edge) with the start edge (static edge) to // maintain the origin text boundary within the selection. final bool isSamePosition = position.offset == existingSelectionStart.offset; final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; final bool shouldSwapEdges = isSelectionInverted != (position.offset < existingSelectionStart.offset) || isSamePosition; if (shouldSwapEdges) { final _TextBoundaryRecord localTextBoundary = getTextBoundary( existingSelectionStart, ); assert( localTextBoundary.boundaryStart.offset >= range.start && localTextBoundary.boundaryEnd.offset <= range.end, ); _setSelectionPosition( isSelectionInverted ? localTextBoundary.boundaryStart : localTextBoundary.boundaryEnd, isEnd: false, ); } } } return targetPosition ?? position; } SelectionResult _updateSelectionEdgeByTextBoundary( Offset globalPosition, { required bool isEnd, required _TextBoundaryAtPosition getTextBoundary, }) { // When the start/end edges are swapped, i.e. the start is after the end, and // the scrollable synthesizes an event for the opposite edge, this will potentially // move the opposite edge outside of the origin text boundary and we are unable to recover. final TextPosition? existingSelectionStart = _textSelectionStart; final TextPosition? existingSelectionEnd = _textSelectionEnd; _setSelectionPosition(null, isEnd: isEnd); final Matrix4 transform = paragraph.getTransformTo(null)..invert(); final Offset localPosition = MatrixUtils.transformPoint( transform, globalPosition, ); if (_rect.isEmpty) { return SelectionUtils.getResultBasedOnRect(_rect, localPosition); } final Offset adjustedOffset = SelectionUtils.adjustDragOffset( _rect, localPosition, direction: paragraph.textDirection, ); final TextPosition position = paragraph.getPositionForOffset( adjustedOffset, ); // Check if the original local position is within the rect, if it is not then // we do not need to look up the text boundary for that position. This is to // maintain a selectables selection collapsed at 0 when the local position is // not located inside its rect. _TextBoundaryRecord? textBoundary = _rect.contains(localPosition) ? getTextBoundary(position) : null; if (textBoundary != null && (textBoundary.boundaryStart.offset < range.start && textBoundary.boundaryEnd.offset <= range.start || textBoundary.boundaryStart.offset >= range.end && textBoundary.boundaryEnd.offset > range.end)) { // When the position is located at a placeholder inside of the text, then we may compute // a text boundary that does not belong to the current selectable fragment. In this case // we should invalidate the text boundary so that it is not taken into account when // computing the target position. textBoundary = null; } final TextPosition targetPosition = _clampTextPosition( isEnd ? _updateSelectionEndEdgeByTextBoundary( textBoundary, getTextBoundary, position, existingSelectionStart, existingSelectionEnd, ) : _updateSelectionStartEdgeByTextBoundary( textBoundary, getTextBoundary, position, existingSelectionStart, existingSelectionEnd, ), ); _setSelectionPosition(targetPosition, isEnd: isEnd); if (targetPosition.offset == range.end) { return SelectionResult.next; } if (targetPosition.offset == range.start) { return SelectionResult.previous; } // TODO(chunhtai): The geometry information should not be used to determine // selection result. This is a workaround to RenderParagraph, where it does // not have a way to get accurate text length if its text is truncated due to // layout constraint. return SelectionUtils.getResultBasedOnRect(_rect, localPosition); } SelectionResult _updateSelectionEdge( Offset globalPosition, { required bool isEnd, }) { _setSelectionPosition(null, isEnd: isEnd); final Matrix4 transform = paragraph.getTransformTo(null)..invert(); final Offset localPosition = MatrixUtils.transformPoint( transform, globalPosition, ); if (_rect.isEmpty) { return SelectionUtils.getResultBasedOnRect(_rect, localPosition); } final Offset adjustedOffset = SelectionUtils.adjustDragOffset( _rect, localPosition, direction: paragraph.textDirection, ); final TextPosition position = _clampTextPosition( paragraph.getPositionForOffset(adjustedOffset), ); _setSelectionPosition(position, isEnd: isEnd); if (position.offset == range.end) { return SelectionResult.next; } if (position.offset == range.start) { return SelectionResult.previous; } // TODO(chunhtai): The geometry information should not be used to determine // selection result. This is a workaround to RenderParagraph, where it does // not have a way to get accurate text length if its text is truncated due to // layout constraint. return SelectionUtils.getResultBasedOnRect(_rect, localPosition); } // This method handles updating the start edge by a text boundary that may // not be contained within this selectable fragment. It is possible // that a boundary spans multiple selectable fragments when the text contains // [WidgetSpan]s. // // This method differs from [_updateSelectionStartEdgeByTextBoundary] in that // to pivot offset used to swap selection edges and maintain the origin // text boundary selected may be located outside of this selectable fragment. // // See [_updateSelectionEndEdgeByMultiSelectableTextBoundary] for the method // that handles updating the end edge. SelectionResult? _updateSelectionStartEdgeByMultiSelectableTextBoundary( _TextBoundaryAtPositionInText getTextBoundary, bool paragraphContainsPosition, TextPosition position, TextPosition? existingSelectionStart, TextPosition? existingSelectionEnd, ) { const bool isEnd = false; if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { // If this selectable contains the origin boundary, maintain the existing // selection. final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset; if (paragraphContainsPosition) { // When the position is within the root paragraph, swap the start and end // edges when the selection is inverted. final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( position, fullText, ); // To accurately retrieve the origin text boundary when the selection // is forward, use existingSelectionEnd.offset - 1. This is necessary // because in a forwards selection, existingSelectionEnd marks the end // of the origin text boundary. Using the unmodified offset incorrectly // targets the subsequent text boundary. final _TextBoundaryRecord originTextBoundary = getTextBoundary( forwardSelection ? TextPosition( offset: existingSelectionEnd.offset - 1, affinity: existingSelectionEnd.affinity, ) : existingSelectionEnd, fullText, ); final TextPosition targetPosition; final int pivotOffset = forwardSelection ? originTextBoundary.boundaryEnd.offset : originTextBoundary.boundaryStart.offset; final bool shouldSwapEdges = !forwardSelection != (position.offset > pivotOffset); if (position.offset < pivotOffset) { targetPosition = boundaryAtPosition.boundaryStart; } else if (position.offset > pivotOffset) { targetPosition = boundaryAtPosition.boundaryEnd; } else { // Keep the origin text boundary in bounds when position is at the static edge. targetPosition = forwardSelection ? existingSelectionStart : existingSelectionEnd; } if (shouldSwapEdges) { _setSelectionPosition( _clampTextPosition( forwardSelection ? originTextBoundary.boundaryStart : originTextBoundary.boundaryEnd, ), isEnd: true, ); } _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); final bool finalSelectionIsForward = _textSelectionEnd!.offset >= _textSelectionStart!.offset; if (boundaryAtPosition.boundaryStart.offset > range.end && boundaryAtPosition.boundaryEnd.offset > range.end) { return SelectionResult.next; } if (boundaryAtPosition.boundaryStart.offset < range.start && boundaryAtPosition.boundaryEnd.offset < range.start) { return SelectionResult.previous; } if (finalSelectionIsForward) { if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) { return SelectionResult.previous; } } else { if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) { return SelectionResult.next; } } } else { // When the drag position is not contained within the root paragraph, // swap the edges when the selection changes direction. final TextPosition clampedPosition = _clampTextPosition(position); // To accurately retrieve the origin text boundary when the selection // is forward, use existingSelectionEnd.offset - 1. This is necessary // because in a forwards selection, existingSelectionEnd marks the end // of the origin text boundary. Using the unmodified offset incorrectly // targets the subsequent text boundary. final _TextBoundaryRecord originTextBoundary = getTextBoundary( forwardSelection ? TextPosition( offset: existingSelectionEnd.offset - 1, affinity: existingSelectionEnd.affinity, ) : existingSelectionEnd, fullText, ); if (forwardSelection && clampedPosition.offset == range.start) { _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.previous; } if (!forwardSelection && clampedPosition.offset == range.end) { _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.next; } if (forwardSelection && clampedPosition.offset == range.end) { _setSelectionPosition( _clampTextPosition(originTextBoundary.boundaryStart), isEnd: true, ); _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.next; } if (!forwardSelection && clampedPosition.offset == range.start) { _setSelectionPosition( _clampTextPosition(originTextBoundary.boundaryEnd), isEnd: true, ); _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.previous; } } } else { // A paragraph boundary may not be completely contained within this root // selectable fragment. Keep searching until we find the end of the // boundary. Do not search when the current drag position is on a placeholder // to allow traversal to reach that placeholder. final bool positionOnPlaceholder = paragraph.getWordBoundary(position).textInside(fullText) == _placeholderCharacter; if (!paragraphContainsPosition || positionOnPlaceholder) { return null; } if (existingSelectionEnd != null) { final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( position, fullText, ); final bool backwardSelection = existingSelectionStart == null && existingSelectionEnd.offset == range.start || existingSelectionStart == existingSelectionEnd && existingSelectionEnd.offset == range.start || existingSelectionStart != null && existingSelectionStart.offset > existingSelectionEnd.offset; if (boundaryAtPosition.boundaryStart.offset < range.start && boundaryAtPosition.boundaryEnd.offset < range.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } if (boundaryAtPosition.boundaryStart.offset > range.end && boundaryAtPosition.boundaryEnd.offset > range.end) { _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (backwardSelection) { if (boundaryAtPosition.boundaryEnd.offset <= range.end) { _setSelectionPosition( _clampTextPosition(boundaryAtPosition.boundaryEnd), isEnd: isEnd, ); return SelectionResult.end; } if (boundaryAtPosition.boundaryEnd.offset > range.end) { _setSelectionPosition( TextPosition(offset: range.end), isEnd: isEnd, ); return SelectionResult.next; } } else { _setSelectionPosition( _clampTextPosition(boundaryAtPosition.boundaryStart), isEnd: isEnd, ); if (boundaryAtPosition.boundaryStart.offset < range.start) { return SelectionResult.previous; } if (boundaryAtPosition.boundaryStart.offset >= range.start) { return SelectionResult.end; } } } } return null; } // This method handles updating the end edge by a text boundary that may // not be contained within this selectable fragment. It is possible // that a boundary spans multiple selectable fragments when the text contains // [WidgetSpan]s. // // This method differs from [_updateSelectionEndEdgeByTextBoundary] in that // to pivot offset used to swap selection edges and maintain the origin // text boundary selected may be located outside of this selectable fragment. // // See [_updateSelectionStartEdgeByMultiSelectableTextBoundary] for the method // that handles updating the end edge. SelectionResult? _updateSelectionEndEdgeByMultiSelectableTextBoundary( _TextBoundaryAtPositionInText getTextBoundary, bool paragraphContainsPosition, TextPosition position, TextPosition? existingSelectionStart, TextPosition? existingSelectionEnd, ) { const bool isEnd = true; if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { // If this selectable contains the origin boundary, maintain the existing // selection. final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset; if (paragraphContainsPosition) { // When the position is within the root paragraph, swap the start and end // edges when the selection is inverted. final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( position, fullText, ); // To accurately retrieve the origin text boundary when the selection // is backwards, use existingSelectionStart.offset - 1. This is necessary // because in a backwards selection, existingSelectionStart marks the end // of the origin text boundary. Using the unmodified offset incorrectly // targets the subsequent text boundary. final _TextBoundaryRecord originTextBoundary = getTextBoundary( forwardSelection ? existingSelectionStart : TextPosition( offset: existingSelectionStart.offset - 1, affinity: existingSelectionStart.affinity, ), fullText, ); final TextPosition targetPosition; final int pivotOffset = forwardSelection ? originTextBoundary.boundaryStart.offset : originTextBoundary.boundaryEnd.offset; final bool shouldSwapEdges = !forwardSelection != (position.offset < pivotOffset); if (position.offset < pivotOffset) { targetPosition = boundaryAtPosition.boundaryStart; } else if (position.offset > pivotOffset) { targetPosition = boundaryAtPosition.boundaryEnd; } else { // Keep the origin text boundary in bounds when position is at the static edge. targetPosition = forwardSelection ? existingSelectionEnd : existingSelectionStart; } if (shouldSwapEdges) { _setSelectionPosition( _clampTextPosition( forwardSelection ? originTextBoundary.boundaryEnd : originTextBoundary.boundaryStart, ), isEnd: false, ); } _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); final bool finalSelectionIsForward = _textSelectionEnd!.offset >= _textSelectionStart!.offset; if (boundaryAtPosition.boundaryStart.offset > range.end && boundaryAtPosition.boundaryEnd.offset > range.end) { return SelectionResult.next; } if (boundaryAtPosition.boundaryStart.offset < range.start && boundaryAtPosition.boundaryEnd.offset < range.start) { return SelectionResult.previous; } if (finalSelectionIsForward) { if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) { return SelectionResult.next; } } else { if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) { return SelectionResult.previous; } } } else { // When the drag position is not contained within the root paragraph, // swap the edges when the selection changes direction. final TextPosition clampedPosition = _clampTextPosition(position); // To accurately retrieve the origin text boundary when the selection // is backwards, use existingSelectionStart.offset - 1. This is necessary // because in a backwards selection, existingSelectionStart marks the end // of the origin text boundary. Using the unmodified offset incorrectly // targets the subsequent text boundary. final _TextBoundaryRecord originTextBoundary = getTextBoundary( forwardSelection ? existingSelectionStart : TextPosition( offset: existingSelectionStart.offset - 1, affinity: existingSelectionStart.affinity, ), fullText, ); if (forwardSelection && clampedPosition.offset == range.start) { _setSelectionPosition( _clampTextPosition(originTextBoundary.boundaryEnd), isEnd: false, ); _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.previous; } if (!forwardSelection && clampedPosition.offset == range.end) { _setSelectionPosition( _clampTextPosition(originTextBoundary.boundaryStart), isEnd: false, ); _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.next; } if (forwardSelection && clampedPosition.offset == range.end) { _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.next; } if (!forwardSelection && clampedPosition.offset == range.start) { _setSelectionPosition(clampedPosition, isEnd: isEnd); return SelectionResult.previous; } } } else { // A paragraph boundary may not be completely contained within this root // selectable fragment. Keep searching until we find the end of the // boundary. Do not search when the current drag position is on a placeholder // to allow traversal to reach that placeholder. final bool positionOnPlaceholder = paragraph.getWordBoundary(position).textInside(fullText) == _placeholderCharacter; if (!paragraphContainsPosition || positionOnPlaceholder) { return null; } if (existingSelectionStart != null) { final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( position, fullText, ); final bool backwardSelection = existingSelectionEnd == null && existingSelectionStart.offset == range.end || existingSelectionStart == existingSelectionEnd && existingSelectionStart.offset == range.end || existingSelectionEnd != null && existingSelectionStart.offset > existingSelectionEnd.offset; if (boundaryAtPosition.boundaryStart.offset < range.start && boundaryAtPosition.boundaryEnd.offset < range.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } if (boundaryAtPosition.boundaryStart.offset > range.end && boundaryAtPosition.boundaryEnd.offset > range.end) { _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (backwardSelection) { _setSelectionPosition( _clampTextPosition(boundaryAtPosition.boundaryStart), isEnd: isEnd, ); if (boundaryAtPosition.boundaryStart.offset < range.start) { return SelectionResult.previous; } if (boundaryAtPosition.boundaryStart.offset >= range.start) { return SelectionResult.end; } } else { if (boundaryAtPosition.boundaryEnd.offset <= range.end) { _setSelectionPosition( _clampTextPosition(boundaryAtPosition.boundaryEnd), isEnd: isEnd, ); return SelectionResult.end; } if (boundaryAtPosition.boundaryEnd.offset > range.end) { _setSelectionPosition( TextPosition(offset: range.end), isEnd: isEnd, ); return SelectionResult.next; } } } } return null; } // The placeholder character used by [RenderParagraph]. static final String _placeholderCharacter = String.fromCharCode( PlaceholderSpan.placeholderCodeUnit, ); static final int _placeholderLength = _placeholderCharacter.length; // This method handles updating the start edge by a text boundary that may // not be contained within this selectable fragment. It is possible // that a boundary spans multiple selectable fragments when the text contains // [WidgetSpan]s. // // This method differs from [_updateSelectionStartEdgeByMultiSelectableBoundary] // in that to maintain the origin text boundary selected at a placeholder, // this selectable fragment must be aware of the [RenderParagraph] that closely // encompasses the complete origin text boundary. // // See [_updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary] for the method // that handles updating the end edge. SelectionResult? _updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary( _TextBoundaryAtPositionInText getTextBoundary, Offset globalPosition, bool paragraphContainsPosition, TextPosition position, TextPosition? existingSelectionStart, TextPosition? existingSelectionEnd, ) { const bool isEnd = false; if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { // If this selectable contains the origin boundary, maintain the existing // selection. final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset; final RenderParagraph originParagraph = _getOriginParagraph(); final bool fragmentBelongsToOriginParagraph = originParagraph == paragraph; if (fragmentBelongsToOriginParagraph) { return _updateSelectionStartEdgeByMultiSelectableTextBoundary( getTextBoundary, paragraphContainsPosition, position, existingSelectionStart, existingSelectionEnd, ); } final Matrix4 originTransform = originParagraph.getTransformTo(null) ..invert(); final Offset originParagraphLocalPosition = MatrixUtils.transformPoint( originTransform, globalPosition, ); final bool positionWithinOriginParagraph = originParagraph.paintBounds .contains( originParagraphLocalPosition, ); final TextPosition positionRelativeToOriginParagraph = originParagraph .getPositionForOffset( originParagraphLocalPosition, ); if (positionWithinOriginParagraph) { // When the selection is inverted by the new position it is necessary to // swap the start edge (moving edge) with the end edge (static edge) to // maintain the origin text boundary within the selection. final String originText = originParagraph.text.toPlainText( includeSemanticsLabels: false, ); final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( positionRelativeToOriginParagraph, originText, ); final _TextBoundaryRecord originTextBoundary = getTextBoundary( _getPositionInParagraph(originParagraph), originText, ); final TextPosition targetPosition; final int pivotOffset = forwardSelection ? originTextBoundary.boundaryEnd.offset : originTextBoundary.boundaryStart.offset; final bool shouldSwapEdges = !forwardSelection != (positionRelativeToOriginParagraph.offset > pivotOffset); if (positionRelativeToOriginParagraph.offset < pivotOffset) { targetPosition = boundaryAtPosition.boundaryStart; } else if (positionRelativeToOriginParagraph.offset > pivotOffset) { targetPosition = boundaryAtPosition.boundaryEnd; } else { // Keep the origin text boundary in bounds when position is at the static edge. targetPosition = existingSelectionStart; } if (shouldSwapEdges) { _setSelectionPosition(existingSelectionStart, isEnd: true); } _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); final bool finalSelectionIsForward = _textSelectionEnd!.offset >= _textSelectionStart!.offset; final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph( originParagraph, ); final TextRange originParagraphPlaceholderRange = TextRange( start: originParagraphPlaceholderTextPosition.offset, end: originParagraphPlaceholderTextPosition.offset + _placeholderLength, ); if (boundaryAtPosition.boundaryStart.offset > originParagraphPlaceholderRange.end && boundaryAtPosition.boundaryEnd.offset > originParagraphPlaceholderRange.end) { return SelectionResult.next; } if (boundaryAtPosition.boundaryStart.offset < originParagraphPlaceholderRange.start && boundaryAtPosition.boundaryEnd.offset < originParagraphPlaceholderRange.start) { return SelectionResult.previous; } if (finalSelectionIsForward) { if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) { return SelectionResult.next; } } else { if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) { return SelectionResult.previous; } } } else { // When the drag position is not contained within the origin paragraph, // swap the edges when the selection changes direction. // // [SelectionUtils.adjustDragOffset] will adjust the given [Offset] to the // beginning or end of the provided [Rect] based on whether the [Offset] // is located within the given [Rect]. final Offset adjustedOffset = SelectionUtils.adjustDragOffset( originParagraph.paintBounds, originParagraphLocalPosition, direction: paragraph.textDirection, ); final TextPosition adjustedPositionRelativeToOriginParagraph = originParagraph.getPositionForOffset(adjustedOffset); final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph( originParagraph, ); final TextRange originParagraphPlaceholderRange = TextRange( start: originParagraphPlaceholderTextPosition.offset, end: originParagraphPlaceholderTextPosition.offset + _placeholderLength, ); if (forwardSelection && adjustedPositionRelativeToOriginParagraph.offset <= originParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } if (!forwardSelection && adjustedPositionRelativeToOriginParagraph.offset >= originParagraphPlaceholderRange.end) { _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (forwardSelection && adjustedPositionRelativeToOriginParagraph.offset >= originParagraphPlaceholderRange.end) { _setSelectionPosition(existingSelectionStart, isEnd: true); _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (!forwardSelection && adjustedPositionRelativeToOriginParagraph.offset <= originParagraphPlaceholderRange.start) { _setSelectionPosition(existingSelectionStart, isEnd: true); _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } } } else { // When the drag position is somewhere on the root text and not a placeholder, // traverse the selectable fragments relative to the [RenderParagraph] that // contains the drag position. if (paragraphContainsPosition) { return _updateSelectionStartEdgeByMultiSelectableTextBoundary( getTextBoundary, paragraphContainsPosition, position, existingSelectionStart, existingSelectionEnd, ); } if (existingSelectionEnd != null) { final ({RenderParagraph paragraph, Offset localPosition})? targetDetails = _getParagraphContainingPosition(globalPosition); if (targetDetails == null) { return null; } final RenderParagraph targetParagraph = targetDetails.paragraph; final TextPosition positionRelativeToTargetParagraph = targetParagraph .getPositionForOffset( targetDetails.localPosition, ); final String targetText = targetParagraph.text.toPlainText( includeSemanticsLabels: false, ); final bool positionOnPlaceholder = targetParagraph .getWordBoundary(positionRelativeToTargetParagraph) .textInside(targetText) == _placeholderCharacter; if (positionOnPlaceholder) { return null; } final bool backwardSelection = existingSelectionStart == null && existingSelectionEnd.offset == range.start || existingSelectionStart == existingSelectionEnd && existingSelectionEnd.offset == range.start || existingSelectionStart != null && existingSelectionStart.offset > existingSelectionEnd.offset; final _TextBoundaryRecord boundaryAtPositionRelativeToTargetParagraph = getTextBoundary( positionRelativeToTargetParagraph, targetText, ); final TextPosition targetParagraphPlaceholderTextPosition = _getPositionInParagraph( targetParagraph, ); final TextRange targetParagraphPlaceholderRange = TextRange( start: targetParagraphPlaceholderTextPosition.offset, end: targetParagraphPlaceholderTextPosition.offset + _placeholderLength, ); if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < targetParagraphPlaceholderRange.start && boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset < targetParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset > targetParagraphPlaceholderRange.end && boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > targetParagraphPlaceholderRange.end) { _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (backwardSelection) { if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <= targetParagraphPlaceholderRange.end) { _setSelectionPosition( TextPosition(offset: range.end), isEnd: isEnd, ); return SelectionResult.end; } if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > targetParagraphPlaceholderRange.end) { _setSelectionPosition( TextPosition(offset: range.end), isEnd: isEnd, ); return SelectionResult.next; } } else { if (boundaryAtPositionRelativeToTargetParagraph .boundaryStart .offset >= targetParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.end; } if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < targetParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } } } } return null; } // This method handles updating the end edge by a text boundary that may // not be contained within this selectable fragment. It is possible // that a boundary spans multiple selectable fragments when the text contains // [WidgetSpan]s. // // This method differs from [_updateSelectionEndEdgeByMultiSelectableBoundary] // in that to maintain the origin text boundary selected at a placeholder, this // selectable fragment must be aware of the [RenderParagraph] that closely // encompasses the complete origin text boundary. // // See [_updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary] // for the method that handles updating the start edge. SelectionResult? _updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary( _TextBoundaryAtPositionInText getTextBoundary, Offset globalPosition, bool paragraphContainsPosition, TextPosition position, TextPosition? existingSelectionStart, TextPosition? existingSelectionEnd, ) { const bool isEnd = true; if (_selectableContainsOriginTextBoundary && existingSelectionStart != null && existingSelectionEnd != null) { // If this selectable contains the origin boundary, maintain the existing // selection. final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset; final RenderParagraph originParagraph = _getOriginParagraph(); final bool fragmentBelongsToOriginParagraph = originParagraph == paragraph; if (fragmentBelongsToOriginParagraph) { return _updateSelectionEndEdgeByMultiSelectableTextBoundary( getTextBoundary, paragraphContainsPosition, position, existingSelectionStart, existingSelectionEnd, ); } final Matrix4 originTransform = originParagraph.getTransformTo(null) ..invert(); final Offset originParagraphLocalPosition = MatrixUtils.transformPoint( originTransform, globalPosition, ); final bool positionWithinOriginParagraph = originParagraph.paintBounds .contains( originParagraphLocalPosition, ); final TextPosition positionRelativeToOriginParagraph = originParagraph .getPositionForOffset( originParagraphLocalPosition, ); if (positionWithinOriginParagraph) { // When the selection is inverted by the new position it is necessary to // swap the end edge (moving edge) with the start edge (static edge) to // maintain the origin text boundary within the selection. final String originText = originParagraph.text.toPlainText( includeSemanticsLabels: false, ); final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( positionRelativeToOriginParagraph, originText, ); final _TextBoundaryRecord originTextBoundary = getTextBoundary( _getPositionInParagraph(originParagraph), originText, ); final TextPosition targetPosition; final int pivotOffset = forwardSelection ? originTextBoundary.boundaryStart.offset : originTextBoundary.boundaryEnd.offset; final bool shouldSwapEdges = !forwardSelection != (positionRelativeToOriginParagraph.offset < pivotOffset); if (positionRelativeToOriginParagraph.offset < pivotOffset) { targetPosition = boundaryAtPosition.boundaryStart; } else if (positionRelativeToOriginParagraph.offset > pivotOffset) { targetPosition = boundaryAtPosition.boundaryEnd; } else { // Keep the origin text boundary in bounds when position is at the static edge. targetPosition = existingSelectionEnd; } if (shouldSwapEdges) { _setSelectionPosition(existingSelectionEnd, isEnd: false); } _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); final bool finalSelectionIsForward = _textSelectionEnd!.offset >= _textSelectionStart!.offset; final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph( originParagraph, ); final TextRange originParagraphPlaceholderRange = TextRange( start: originParagraphPlaceholderTextPosition.offset, end: originParagraphPlaceholderTextPosition.offset + _placeholderLength, ); if (boundaryAtPosition.boundaryStart.offset > originParagraphPlaceholderRange.end && boundaryAtPosition.boundaryEnd.offset > originParagraphPlaceholderRange.end) { return SelectionResult.next; } if (boundaryAtPosition.boundaryStart.offset < originParagraphPlaceholderRange.start && boundaryAtPosition.boundaryEnd.offset < originParagraphPlaceholderRange.start) { return SelectionResult.previous; } if (finalSelectionIsForward) { if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) { return SelectionResult.next; } } else { if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) { return SelectionResult.end; } if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) { return SelectionResult.previous; } } } else { // When the drag position is not contained within the origin paragraph, // swap the edges when the selection changes direction. // // [SelectionUtils.adjustDragOffset] will adjust the given [Offset] to the // beginning or end of the provided [Rect] based on whether the [Offset] // is located within the given [Rect]. final Offset adjustedOffset = SelectionUtils.adjustDragOffset( originParagraph.paintBounds, originParagraphLocalPosition, direction: paragraph.textDirection, ); final TextPosition adjustedPositionRelativeToOriginParagraph = originParagraph.getPositionForOffset(adjustedOffset); final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph( originParagraph, ); final TextRange originParagraphPlaceholderRange = TextRange( start: originParagraphPlaceholderTextPosition.offset, end: originParagraphPlaceholderTextPosition.offset + _placeholderLength, ); if (forwardSelection && adjustedPositionRelativeToOriginParagraph.offset <= originParagraphPlaceholderRange.start) { _setSelectionPosition(existingSelectionEnd, isEnd: false); _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } if (!forwardSelection && adjustedPositionRelativeToOriginParagraph.offset >= originParagraphPlaceholderRange.end) { _setSelectionPosition(existingSelectionEnd, isEnd: false); _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (forwardSelection && adjustedPositionRelativeToOriginParagraph.offset >= originParagraphPlaceholderRange.end) { _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (!forwardSelection && adjustedPositionRelativeToOriginParagraph.offset <= originParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } } } else { // When the drag position is somewhere on the root text and not a placeholder, // traverse the selectable fragments relative to the [RenderParagraph] that // contains the drag position. if (paragraphContainsPosition) { return _updateSelectionEndEdgeByMultiSelectableTextBoundary( getTextBoundary, paragraphContainsPosition, position, existingSelectionStart, existingSelectionEnd, ); } if (existingSelectionStart != null) { final ({RenderParagraph paragraph, Offset localPosition})? targetDetails = _getParagraphContainingPosition(globalPosition); if (targetDetails == null) { return null; } final RenderParagraph targetParagraph = targetDetails.paragraph; final TextPosition positionRelativeToTargetParagraph = targetParagraph .getPositionForOffset( targetDetails.localPosition, ); final String targetText = targetParagraph.text.toPlainText( includeSemanticsLabels: false, ); final bool positionOnPlaceholder = targetParagraph .getWordBoundary(positionRelativeToTargetParagraph) .textInside(targetText) == _placeholderCharacter; if (positionOnPlaceholder) { return null; } final bool backwardSelection = existingSelectionEnd == null && existingSelectionStart.offset == range.end || existingSelectionStart == existingSelectionEnd && existingSelectionStart.offset == range.end || existingSelectionEnd != null && existingSelectionStart.offset > existingSelectionEnd.offset; final _TextBoundaryRecord boundaryAtPositionRelativeToTargetParagraph = getTextBoundary( positionRelativeToTargetParagraph, targetText, ); final TextPosition targetParagraphPlaceholderTextPosition = _getPositionInParagraph( targetParagraph, ); final TextRange targetParagraphPlaceholderRange = TextRange( start: targetParagraphPlaceholderTextPosition.offset, end: targetParagraphPlaceholderTextPosition.offset + _placeholderLength, ); if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < targetParagraphPlaceholderRange.start && boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset < targetParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset > targetParagraphPlaceholderRange.end && boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > targetParagraphPlaceholderRange.end) { _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); return SelectionResult.next; } if (backwardSelection) { if (boundaryAtPositionRelativeToTargetParagraph .boundaryStart .offset >= targetParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.end; } if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < targetParagraphPlaceholderRange.start) { _setSelectionPosition( TextPosition(offset: range.start), isEnd: isEnd, ); return SelectionResult.previous; } } else { if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <= targetParagraphPlaceholderRange.end) { _setSelectionPosition( TextPosition(offset: range.end), isEnd: isEnd, ); return SelectionResult.end; } if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > targetParagraphPlaceholderRange.end) { _setSelectionPosition( TextPosition(offset: range.end), isEnd: isEnd, ); return SelectionResult.next; } } } } return null; } SelectionResult _updateSelectionEdgeByMultiSelectableTextBoundary( Offset globalPosition, { required bool isEnd, required _TextBoundaryAtPositionInText getTextBoundary, required _TextBoundaryAtPosition getClampedTextBoundary, }) { // When the start/end edges are swapped, i.e. the start is after the end, and // the scrollable synthesizes an event for the opposite edge, this will potentially // move the opposite edge outside of the origin text boundary and we are unable to recover. final TextPosition? existingSelectionStart = _textSelectionStart; final TextPosition? existingSelectionEnd = _textSelectionEnd; _setSelectionPosition(null, isEnd: isEnd); final Matrix4 transform = paragraph.getTransformTo(null); transform.invert(); final Offset localPosition = MatrixUtils.transformPoint( transform, globalPosition, ); if (_rect.isEmpty) { return SelectionUtils.getResultBasedOnRect(_rect, localPosition); } final Offset adjustedOffset = SelectionUtils.adjustDragOffset( _rect, localPosition, direction: paragraph.textDirection, ); final Offset adjustedOffsetRelativeToParagraph = SelectionUtils.adjustDragOffset( paragraph.paintBounds, localPosition, direction: paragraph.textDirection, ); final TextPosition position = paragraph.getPositionForOffset( adjustedOffset, ); final TextPosition positionInFullText = paragraph.getPositionForOffset( adjustedOffsetRelativeToParagraph, ); final SelectionResult? result; if (_isPlaceholder()) { result = isEnd ? _updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary( getTextBoundary, globalPosition, paragraph.paintBounds.contains(localPosition), positionInFullText, existingSelectionStart, existingSelectionEnd, ) : _updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary( getTextBoundary, globalPosition, paragraph.paintBounds.contains(localPosition), positionInFullText, existingSelectionStart, existingSelectionEnd, ); } else { result = isEnd ? _updateSelectionEndEdgeByMultiSelectableTextBoundary( getTextBoundary, paragraph.paintBounds.contains(localPosition), positionInFullText, existingSelectionStart, existingSelectionEnd, ) : _updateSelectionStartEdgeByMultiSelectableTextBoundary( getTextBoundary, paragraph.paintBounds.contains(localPosition), positionInFullText, existingSelectionStart, existingSelectionEnd, ); } if (result != null) { return result; } // Check if the original local position is within the rect, if it is not then // we do not need to look up the text boundary for that position. This is to // maintain a selectables selection collapsed at 0 when the local position is // not located inside its rect. _TextBoundaryRecord? textBoundary = _boundingBoxesContains(localPosition) ? getClampedTextBoundary(position) : null; if (textBoundary != null && (textBoundary.boundaryStart.offset < range.start && textBoundary.boundaryEnd.offset <= range.start || textBoundary.boundaryStart.offset >= range.end && textBoundary.boundaryEnd.offset > range.end)) { // When the position is located at a placeholder inside of the text, then we may compute // a text boundary that does not belong to the current selectable fragment. In this case // we should invalidate the text boundary so that it is not taken into account when // computing the target position. textBoundary = null; } final TextPosition targetPosition = _clampTextPosition( isEnd ? _updateSelectionEndEdgeByTextBoundary( textBoundary, getClampedTextBoundary, position, existingSelectionStart, existingSelectionEnd, ) : _updateSelectionStartEdgeByTextBoundary( textBoundary, getClampedTextBoundary, position, existingSelectionStart, existingSelectionEnd, ), ); _setSelectionPosition(targetPosition, isEnd: isEnd); if (targetPosition.offset == range.end) { return SelectionResult.next; } if (targetPosition.offset == range.start) { return SelectionResult.previous; } // TODO(chunhtai): The geometry information should not be used to determine // selection result. This is a workaround to RenderParagraph, where it does // not have a way to get accurate text length if its text is truncated due to // layout constraint. return SelectionUtils.getResultBasedOnRect(_rect, localPosition); } TextPosition _closestTextBoundary( _TextBoundaryRecord textBoundary, TextPosition position, ) { final int differenceA = (position.offset - textBoundary.boundaryStart.offset).abs(); final int differenceB = (position.offset - textBoundary.boundaryEnd.offset) .abs(); return differenceA < differenceB ? textBoundary.boundaryStart : textBoundary.boundaryEnd; } bool _isPlaceholder() { // Determine whether this selectable fragment is a placeholder. RenderObject? current = paragraph.parent; while (current != null) { if (current is RenderParagraph) { return true; } current = current.parent; } return false; } RenderParagraph _getOriginParagraph() { // This method should only be called from a fragment that contains // the origin boundary. By traversing up the RenderTree, determine the // highest RenderParagraph that contains the origin text boundary. assert(_selectableContainsOriginTextBoundary); // Begin at the parent because it is guaranteed the paragraph containing // this selectable fragment contains the origin boundary. RenderObject? current = paragraph.parent; RenderParagraph? originParagraph; while (current != null) { if (current is RenderParagraph) { if (current._lastSelectableFragments != null) { bool paragraphContainsOriginTextBoundary = false; for (final _SelectableFragment fragment in current._lastSelectableFragments!) { if (fragment._selectableContainsOriginTextBoundary) { paragraphContainsOriginTextBoundary = true; originParagraph = current; break; } } if (!paragraphContainsOriginTextBoundary) { return originParagraph ?? paragraph; } } } current = current.parent; } return originParagraph ?? paragraph; } ({RenderParagraph paragraph, Offset localPosition})? _getParagraphContainingPosition( Offset globalPosition, ) { // This method will return the closest [RenderParagraph] whose rect // contains the given `globalPosition` and the given `globalPosition` // relative to that [RenderParagraph]. If no ancestor [RenderParagraph] // contains the given `globalPosition` then this method will return null. RenderObject? current = paragraph; while (current != null) { if (current is RenderParagraph) { final Matrix4 currentTransform = current.getTransformTo(null)..invert(); final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint( currentTransform, globalPosition, ); final bool positionWithinCurrentParagraph = current.paintBounds .contains( currentParagraphLocalPosition, ); if (positionWithinCurrentParagraph) { return ( paragraph: current, localPosition: currentParagraphLocalPosition, ); } } current = current.parent; } return null; } bool _boundingBoxesContains(Offset position) { for (final Rect rect in boundingBoxes) { if (rect.contains(position)) { return true; } } return false; } TextPosition _clampTextPosition(TextPosition position) { // Affinity of range.end is upstream. if (position.offset > range.end || (position.offset == range.end && position.affinity == TextAffinity.downstream)) { return TextPosition(offset: range.end, affinity: TextAffinity.upstream); } if (position.offset < range.start) { return TextPosition(offset: range.start); } return position; } void _setSelectionPosition(TextPosition? position, {required bool isEnd}) { if (isEnd) { _textSelectionEnd = position; } else { _textSelectionStart = position; } } SelectionResult _handleClearSelection() { _textSelectionStart = null; _textSelectionEnd = null; _selectableContainsOriginTextBoundary = false; return SelectionResult.none; } SelectionResult _handleSelectAll() { _textSelectionStart = TextPosition(offset: range.start); _textSelectionEnd = TextPosition( offset: range.end, affinity: TextAffinity.upstream, ); return SelectionResult.none; } SelectionResult _handleSelectTextBoundary(_TextBoundaryRecord textBoundary) { // This fragment may not contain the boundary, decide what direction the target // fragment is located in. Because fragments are separated by placeholder // spans, we also check if the beginning or end of the boundary is touching // either edge of this fragment. if (textBoundary.boundaryStart.offset < range.start && textBoundary.boundaryEnd.offset <= range.start) { return SelectionResult.previous; } else if (textBoundary.boundaryStart.offset >= range.end && textBoundary.boundaryEnd.offset > range.end) { return SelectionResult.next; } // Fragments are separated by placeholder span, the text boundary shouldn't // expand across fragments. assert( textBoundary.boundaryStart.offset >= range.start && textBoundary.boundaryEnd.offset <= range.end, ); _textSelectionStart = textBoundary.boundaryStart; _textSelectionEnd = textBoundary.boundaryEnd; _selectableContainsOriginTextBoundary = true; return SelectionResult.end; } TextRange? _intersect(TextRange a, TextRange b) { assert(a.isNormalized); assert(b.isNormalized); final int startMax = math.max(a.start, b.start); final int endMin = math.min(a.end, b.end); if (startMax <= endMin) { // Intersection. return TextRange(start: startMax, end: endMin); } return null; } SelectionResult _handleSelectMultiFragmentTextBoundary( _TextBoundaryRecord textBoundary, ) { // This fragment may not contain the boundary, decide what direction the target // fragment is located in. Because fragments are separated by placeholder // spans, we also check if the beginning or end of the boundary is touching // either edge of this fragment. if (textBoundary.boundaryStart.offset < range.start && textBoundary.boundaryEnd.offset <= range.start) { return SelectionResult.previous; } else if (textBoundary.boundaryStart.offset >= range.end && textBoundary.boundaryEnd.offset > range.end) { return SelectionResult.next; } final TextRange boundaryAsRange = TextRange( start: textBoundary.boundaryStart.offset, end: textBoundary.boundaryEnd.offset, ); final TextRange? intersectRange = _intersect(range, boundaryAsRange); if (intersectRange != null) { _textSelectionStart = TextPosition(offset: intersectRange.start); _textSelectionEnd = TextPosition(offset: intersectRange.end); _selectableContainsOriginTextBoundary = true; if (range.end < textBoundary.boundaryEnd.offset) { return SelectionResult.next; } return SelectionResult.end; } return SelectionResult.none; } _TextBoundaryRecord _adjustTextBoundaryAtPosition( TextRange textBoundary, TextPosition position, ) { late final TextPosition start; late final TextPosition end; if (position.offset > textBoundary.end) { start = end = TextPosition(offset: position.offset); } else { start = TextPosition(offset: textBoundary.start); end = TextPosition( offset: textBoundary.end, affinity: TextAffinity.upstream, ); } return (boundaryStart: start, boundaryEnd: end); } SelectionResult _handleSelectWord(Offset globalPosition) { final TextPosition position = paragraph.getPositionForOffset( paragraph.globalToLocal(globalPosition), ); if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) { return SelectionResult.end; } final _TextBoundaryRecord wordBoundary = _getWordBoundaryAtPosition( position, ); return _handleSelectTextBoundary(wordBoundary); } _TextBoundaryRecord _getWordBoundaryAtPosition(TextPosition position) { final TextRange word = paragraph.getWordBoundary(position); assert(word.isNormalized); return _adjustTextBoundaryAtPosition(word, position); } SelectionResult _handleSelectParagraph(Offset globalPosition) { final Offset localPosition = paragraph.globalToLocal(globalPosition); final TextPosition position = paragraph.getPositionForOffset(localPosition); final _TextBoundaryRecord paragraphBoundary = _getParagraphBoundaryAtPosition( position, fullText, ); return _handleSelectMultiFragmentTextBoundary(paragraphBoundary); } TextPosition _getPositionInParagraph(RenderParagraph targetParagraph) { final Matrix4 transform = paragraph.getTransformTo(targetParagraph); final Offset localCenter = paragraph.paintBounds.centerLeft; final Offset localPos = MatrixUtils.transformPoint(transform, localCenter); final TextPosition position = targetParagraph.getPositionForOffset( localPos, ); return position; } _TextBoundaryRecord _getParagraphBoundaryAtPosition( TextPosition position, String text, ) { final ParagraphBoundary paragraphBoundary = ParagraphBoundary(text); // Use position.offset - 1 when `position` is at the end of the selectable to retrieve // the previous text boundary's location. final int paragraphStart = paragraphBoundary.getLeadingTextBoundaryAt( position.offset == text.length || position.affinity == TextAffinity.upstream ? position.offset - 1 : position.offset, ) ?? 0; final int paragraphEnd = paragraphBoundary.getTrailingTextBoundaryAt(position.offset) ?? text.length; final TextRange paragraphRange = TextRange( start: paragraphStart, end: paragraphEnd, ); assert(paragraphRange.isNormalized); return _adjustTextBoundaryAtPosition(paragraphRange, position); } _TextBoundaryRecord _getClampedParagraphBoundaryAtPosition( TextPosition position, ) { final ParagraphBoundary paragraphBoundary = ParagraphBoundary(fullText); // Use position.offset - 1 when `position` is at the end of the selectable to retrieve // the previous text boundary's location. int paragraphStart = paragraphBoundary.getLeadingTextBoundaryAt( position.offset == fullText.length || position.affinity == TextAffinity.upstream ? position.offset - 1 : position.offset, ) ?? 0; int paragraphEnd = paragraphBoundary.getTrailingTextBoundaryAt(position.offset) ?? fullText.length; paragraphStart = paragraphStart < range.start ? range.start : paragraphStart > range.end ? range.end : paragraphStart; paragraphEnd = paragraphEnd > range.end ? range.end : paragraphEnd < range.start ? range.start : paragraphEnd; final TextRange paragraphRange = TextRange( start: paragraphStart, end: paragraphEnd, ); assert(paragraphRange.isNormalized); return _adjustTextBoundaryAtPosition(paragraphRange, position); } SelectionResult _handleDirectionallyExtendSelection( double horizontalBaseline, bool isExtent, SelectionExtendDirection movement, ) { final Matrix4 transform = paragraph.getTransformTo(null); if (transform.invert() == 0.0) { switch (movement) { case SelectionExtendDirection.previousLine: case SelectionExtendDirection.backward: return SelectionResult.previous; case SelectionExtendDirection.nextLine: case SelectionExtendDirection.forward: return SelectionResult.next; } } final double baselineInParagraphCoordinates = MatrixUtils.transformPoint( transform, Offset(horizontalBaseline, 0), ).dx; assert(!baselineInParagraphCoordinates.isNaN); final TextPosition newPosition; final SelectionResult result; switch (movement) { case SelectionExtendDirection.previousLine: case SelectionExtendDirection.nextLine: assert(_textSelectionEnd != null && _textSelectionStart != null); final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!; final MapEntry moveResult = _handleVerticalMovement( targetedEdge, horizontalBaselineInParagraphCoordinates: baselineInParagraphCoordinates, below: movement == SelectionExtendDirection.nextLine, ); newPosition = moveResult.key; result = moveResult.value; case SelectionExtendDirection.forward: case SelectionExtendDirection.backward: _textSelectionEnd ??= movement == SelectionExtendDirection.forward ? TextPosition(offset: range.start) : TextPosition(offset: range.end, affinity: TextAffinity.upstream); _textSelectionStart ??= _textSelectionEnd; final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!; final Offset edgeOffsetInParagraphCoordinates = paragraph ._getOffsetForPosition( targetedEdge, ); final Offset baselineOffsetInParagraphCoordinates = Offset( baselineInParagraphCoordinates, // Use half of line height to point to the middle of the line. edgeOffsetInParagraphCoordinates.dy - paragraph._textPainter.preferredLineHeight / 2, ); newPosition = paragraph.getPositionForOffset( baselineOffsetInParagraphCoordinates, ); result = SelectionResult.end; } if (isExtent) { _textSelectionEnd = newPosition; } else { _textSelectionStart = newPosition; } return result; } SelectionResult _handleGranularlyExtendSelection( bool forward, bool isExtent, TextGranularity granularity, ) { _textSelectionEnd ??= forward ? TextPosition(offset: range.start) : TextPosition(offset: range.end, affinity: TextAffinity.upstream); _textSelectionStart ??= _textSelectionEnd; final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!; if (forward && (targetedEdge.offset == range.end)) { return SelectionResult.next; } if (!forward && (targetedEdge.offset == range.start)) { return SelectionResult.previous; } final SelectionResult result; final TextPosition newPosition; switch (granularity) { case TextGranularity.character: final String text = range.textInside(fullText); newPosition = _moveBeyondTextBoundaryAtDirection( targetedEdge, forward, CharacterBoundary(text), ); result = SelectionResult.end; case TextGranularity.word: final TextBoundary textBoundary = paragraph._textPainter.wordBoundaries.moveByWordBoundary; newPosition = _moveBeyondTextBoundaryAtDirection( targetedEdge, forward, textBoundary, ); result = SelectionResult.end; case TextGranularity.paragraph: final String text = range.textInside(fullText); newPosition = _moveBeyondTextBoundaryAtDirection( targetedEdge, forward, ParagraphBoundary(text), ); result = SelectionResult.end; case TextGranularity.line: newPosition = _moveToTextBoundaryAtDirection( targetedEdge, forward, LineBoundary(this), ); result = SelectionResult.end; case TextGranularity.document: final String text = range.textInside(fullText); newPosition = _moveBeyondTextBoundaryAtDirection( targetedEdge, forward, DocumentBoundary(text), ); if (forward && newPosition.offset == range.end) { result = SelectionResult.next; } else if (!forward && newPosition.offset == range.start) { result = SelectionResult.previous; } else { result = SelectionResult.end; } } if (isExtent) { _textSelectionEnd = newPosition; } else { _textSelectionStart = newPosition; } return result; } // Move **beyond** the local boundary of the given type (unless range.start or // range.end is reached). Used for most TextGranularity types except for // TextGranularity.line, to ensure the selection movement doesn't get stuck at // a local fixed point. TextPosition _moveBeyondTextBoundaryAtDirection( TextPosition end, bool forward, TextBoundary textBoundary, ) { final int newOffset = forward ? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end : textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start; return TextPosition(offset: newOffset); } // Move **to** the local boundary of the given type. Typically used for line // boundaries, such that performing "move to line start" more than once never // moves the selection to the previous line. TextPosition _moveToTextBoundaryAtDirection( TextPosition end, bool forward, TextBoundary textBoundary, ) { assert(end.offset >= 0); final int caretOffset; switch (end.affinity) { case TextAffinity.upstream: if (end.offset < 1 && !forward) { assert(end.offset == 0); return const TextPosition(offset: 0); } final CharacterBoundary characterBoundary = CharacterBoundary(fullText); caretOffset = math.max( 0, characterBoundary.getLeadingTextBoundaryAt( range.start + end.offset, ) ?? range.start, ) - 1; case TextAffinity.downstream: caretOffset = end.offset; } final int offset = forward ? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end : textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start; return TextPosition(offset: offset); } MapEntry _handleVerticalMovement( TextPosition position, { required double horizontalBaselineInParagraphCoordinates, required bool below, }) { final List lines = paragraph._textPainter .computeLineMetrics(); final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero); int currentLine = lines.length - 1; for (final ui.LineMetrics lineMetrics in lines) { if (lineMetrics.baseline > offset.dy) { currentLine = lineMetrics.lineNumber; break; } } final TextPosition newPosition; if (below && currentLine == lines.length - 1) { newPosition = TextPosition( offset: range.end, affinity: TextAffinity.upstream, ); } else if (!below && currentLine == 0) { newPosition = TextPosition(offset: range.start); } else { final int newLine = below ? currentLine + 1 : currentLine - 1; newPosition = _clampTextPosition( paragraph.getPositionForOffset( Offset( horizontalBaselineInParagraphCoordinates, lines[newLine].baseline, ), ), ); } final SelectionResult result; if (newPosition.offset == range.start) { result = SelectionResult.previous; } else if (newPosition.offset == range.end) { result = SelectionResult.next; } else { result = SelectionResult.end; } assert(result != SelectionResult.next || below); assert(result != SelectionResult.previous || !below); return MapEntry(newPosition, result); } /// Whether the given text position is contained in current selection /// range. /// /// The parameter `start` must be smaller than `end`. bool _positionIsWithinCurrentSelection(TextPosition position) { if (_textSelectionStart == null || _textSelectionEnd == null) { return false; } // Normalize current selection. late TextPosition currentStart; late TextPosition currentEnd; if (_compareTextPositions(_textSelectionStart!, _textSelectionEnd!) > 0) { currentStart = _textSelectionStart!; currentEnd = _textSelectionEnd!; } else { currentStart = _textSelectionEnd!; currentEnd = _textSelectionStart!; } return _compareTextPositions(currentStart, position) >= 0 && _compareTextPositions(currentEnd, position) <= 0; } /// Compares two text positions. /// /// Returns 1 if `position` < `otherPosition`, -1 if `position` > `otherPosition`, /// or 0 if they are equal. static int _compareTextPositions( TextPosition position, TextPosition otherPosition, ) { if (position.offset < otherPosition.offset) { return 1; } else if (position.offset > otherPosition.offset) { return -1; } else if (position.affinity == otherPosition.affinity) { return 0; } else { return position.affinity == TextAffinity.upstream ? 1 : -1; } } @override Matrix4 getTransformTo(RenderObject? ancestor) { return paragraph.getTransformTo(ancestor); } @override void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { if (!paragraph.attached) { assert( startHandle == null && endHandle == null, 'Only clean up can be called.', ); return; } if (_startHandleLayerLink != startHandle) { _startHandleLayerLink = startHandle; paragraph.markNeedsPaint(); } if (_endHandleLayerLink != endHandle) { _endHandleLayerLink = endHandle; paragraph.markNeedsPaint(); } } List? _cachedBoundingBoxes; @override List get boundingBoxes { if (_cachedBoundingBoxes == null) { final List boxes = paragraph.getBoxesForSelection( TextSelection(baseOffset: range.start, extentOffset: range.end), boxHeightStyle: ui.BoxHeightStyle.max, ); if (boxes.isNotEmpty) { _cachedBoundingBoxes = []; for (final TextBox textBox in boxes) { _cachedBoundingBoxes!.add(textBox.toRect()); } } else { final Offset offset = paragraph._getOffsetForPosition( TextPosition(offset: range.start), ); final Rect rect = Rect.fromPoints( offset, offset.translate(0, -paragraph._textPainter.preferredLineHeight), ); _cachedBoundingBoxes = [rect]; } } return _cachedBoundingBoxes!; } Rect? _cachedRect; Rect get _rect { if (_cachedRect == null) { final List boxes = paragraph.getBoxesForSelection( TextSelection(baseOffset: range.start, extentOffset: range.end), ); if (boxes.isNotEmpty) { Rect result = boxes.first.toRect(); for (int index = 1; index < boxes.length; index += 1) { result = result.expandToInclude(boxes[index].toRect()); } _cachedRect = result; } else { final Offset offset = paragraph._getOffsetForPosition( TextPosition(offset: range.start), ); _cachedRect = Rect.fromPoints( offset, offset.translate(0, -paragraph._textPainter.preferredLineHeight), ); } } return _cachedRect!; } void didChangeParagraphLayout() { _cachedRect = null; _cachedBoundingBoxes = null; } @override int get contentLength => range.end - range.start; @override Size get size { return _rect.size; } void paint(PaintingContext context, Offset offset) { if (_textSelectionStart == null || _textSelectionEnd == null) { return; } if (paragraph.selectionColor != null) { final TextSelection selection = TextSelection( baseOffset: _textSelectionStart!.offset, extentOffset: _textSelectionEnd!.offset, ); final Paint selectionPaint = Paint() ..style = PaintingStyle.fill ..color = paragraph.selectionColor!; for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) { context.canvas.drawRect(textBox.toRect().shift(offset), selectionPaint); } } if (_startHandleLayerLink != null && value.startSelectionPoint != null) { context.pushLayer( LeaderLayer( link: _startHandleLayerLink!, offset: offset + value.startSelectionPoint!.localPosition, ), (PaintingContext context, Offset offset) {}, Offset.zero, ); } if (_endHandleLayerLink != null && value.endSelectionPoint != null) { context.pushLayer( LeaderLayer( link: _endHandleLayerLink!, offset: offset + value.endSelectionPoint!.localPosition, ), (PaintingContext context, Offset offset) {}, Offset.zero, ); } } @override TextSelection getLineAtOffset(TextPosition position) { final TextRange line = paragraph._getLineAtOffset(position); final int start = line.start.clamp(range.start, range.end); final int end = line.end.clamp(range.start, range.end); return TextSelection(baseOffset: start, extentOffset: end); } @override TextPosition getTextPositionAbove(TextPosition position) { return _clampTextPosition(paragraph._getTextPositionAbove(position)); } @override TextPosition getTextPositionBelow(TextPosition position) { return _clampTextPosition(paragraph._getTextPositionBelow(position)); } @override TextRange getWordBoundary(TextPosition position) => paragraph.getWordBoundary(position); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( DiagnosticsProperty( 'textInsideRange', range.textInside(fullText), ), ) ..add(DiagnosticsProperty('range', range)) ..add(DiagnosticsProperty('fullText', fullText)); } } ================================================ FILE: lib/common/widgets/flutter/text/rich_text.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' as ui show TextHeightBehavior; import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; import 'package:flutter/material.dart' hide RichText; import 'package:flutter/rendering.dart' hide RenderParagraph; /// A paragraph of rich text. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=rykDVh-QFfw} /// /// The [RichText] widget displays text that uses multiple different styles. The /// text to display is described using a tree of [TextSpan] objects, each of /// which has an associated style that is used for that subtree. The text might /// break across multiple lines or might all be displayed on the same line /// depending on the layout constraints. /// /// Text displayed in a [RichText] widget must be explicitly styled. When /// picking which style to use, consider using [DefaultTextStyle.of] the current /// [BuildContext] to provide defaults. For more details on how to style text in /// a [RichText] widget, see the documentation for [TextStyle]. /// /// Consider using the [Text] widget to integrate with the [DefaultTextStyle] /// automatically. When all the text uses the same style, the default constructor /// is less verbose. The [Text.rich] constructor allows you to style multiple /// spans with the default text style while still allowing specified styles per /// span. /// /// {@tool snippet} /// /// This sample demonstrates how to mix and match text with different text /// styles using the [RichText] Widget. It displays the text "Hello bold world," /// emphasizing the word "bold" using a bold font weight. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png) /// /// ```dart /// RichText( /// text: TextSpan( /// text: 'Hello ', /// style: DefaultTextStyle.of(context).style, /// children: const [ /// TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)), /// TextSpan(text: ' world!'), /// ], /// ), /// ) /// ``` /// {@end-tool} /// /// ## Selections /// /// To make this [RichText] Selectable, the [RichText] needs to be in the /// subtree of a [SelectionArea] or [SelectableRegion] and a /// [SelectionRegistrar] needs to be assigned to the /// [RichText.selectionRegistrar]. One can use /// [SelectionContainer.maybeOf] to get the [SelectionRegistrar] from a /// context. This enables users to select the text in [RichText]s with mice or /// touch events. /// /// The [selectionColor] also needs to be set if the selection is enabled to /// draw the selection highlights. /// /// {@tool snippet} /// /// This sample demonstrates how to assign a [SelectionRegistrar] for RichTexts /// in the SelectionArea subtree. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png) /// /// ```dart /// RichText( /// text: const TextSpan(text: 'Hello'), /// selectionRegistrar: SelectionContainer.maybeOf(context), /// selectionColor: const Color(0xAF6694e8), /// ) /// ``` /// {@end-tool} /// /// See also: /// /// * [TextStyle], which discusses how to style text. /// * [TextSpan], which is used to describe the text in a paragraph. /// * [Text], which automatically applies the ambient styles described by a /// [DefaultTextStyle] to a single string. /// * [Text.rich], a const text widget that provides similar functionality /// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle]. /// * [SelectableRegion], which provides an overview of the selection system. class RichText extends MultiChildRenderObjectWidget { /// Creates a paragraph of rich text. /// /// The [maxLines] property may be null (and indeed defaults to null), but if /// it is not null, it must be greater than zero. /// /// The [textDirection], if null, defaults to the ambient [Directionality], /// which in that case must not be null. RichText({ super.key, required this.text, this.textAlign = TextAlign.start, this.textDirection, this.softWrap = true, this.overflow = TextOverflow.clip, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) double textScaleFactor = 1.0, TextScaler textScaler = TextScaler.noScaling, this.maxLines, this.locale, this.strutStyle, this.textWidthBasis = TextWidthBasis.parent, this.textHeightBehavior, this.selectionRegistrar, this.selectionColor, required this.primary, this.onShowMore, }) : assert(maxLines == null || maxLines > 0), assert(selectionRegistrar == null || selectionColor != null), assert( textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), 'Use textScaler instead.', ), textScaler = _effectiveTextScalerFrom(textScaler, textScaleFactor), super( children: WidgetSpan.extractFromInlineSpan( text, _effectiveTextScalerFrom(textScaler, textScaleFactor), ), ); static TextScaler _effectiveTextScalerFrom( TextScaler textScaler, double textScaleFactor, ) { return switch ((textScaler, textScaleFactor)) { (final TextScaler scaler, 1.0) => scaler, (TextScaler.noScaling, final double textScaleFactor) => TextScaler.linear( textScaleFactor, ), (final TextScaler scaler, _) => scaler, }; } /// The text to display in this widget. final InlineSpan text; /// How the text should be aligned horizontally. final TextAlign textAlign; /// The directionality of the text. /// /// This decides how [textAlign] values like [TextAlign.start] and /// [TextAlign.end] are interpreted. /// /// This is also used to disambiguate how to render bidirectional text. For /// example, if the [text] is an English phrase followed by a Hebrew phrase, /// in a [TextDirection.ltr] context the English phrase will be on the left /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. /// /// Defaults to the ambient [Directionality], if any. If there is no ambient /// [Directionality], then this must not be null. final TextDirection? textDirection; /// Whether the text should break at soft line breaks. /// /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. final bool softWrap; /// How visual overflow should be handled. final TextOverflow overflow; /// Deprecated. Will be removed in a future version of Flutter. Use /// [textScaler] instead. /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) double get textScaleFactor => textScaler.textScaleFactor; /// {@macro flutter.painting.textPainter.textScaler} final TextScaler textScaler; /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. /// /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the /// edge of the box. final int? maxLines; /// Used to select a font when the same Unicode character can /// be rendered differently, depending on the locale. /// /// It's rarely necessary to set this property. By default its value /// is inherited from the enclosing app with `Localizations.localeOf(context)`. /// /// See [RenderParagraph.locale] for more information. final Locale? locale; /// {@macro flutter.painting.textPainter.strutStyle} final StrutStyle? strutStyle; /// {@macro flutter.painting.textPainter.textWidthBasis} final TextWidthBasis textWidthBasis; /// {@macro dart.ui.textHeightBehavior} final ui.TextHeightBehavior? textHeightBehavior; /// The [SelectionRegistrar] this rich text is subscribed to. /// /// If this is set, [selectionColor] must be non-null. final SelectionRegistrar? selectionRegistrar; /// The color to use when painting the selection. /// /// This is ignored if [selectionRegistrar] is null. /// /// See the section on selections in the [RichText] top-level API /// documentation for more details on enabling selection in [RichText] /// widgets. final Color? selectionColor; final Color primary; final VoidCallback? onShowMore; @override RenderParagraph createRenderObject(BuildContext context) { assert(textDirection != null || debugCheckHasDirectionality(context)); return RenderParagraph( text, textAlign: textAlign, textDirection: textDirection ?? Directionality.of(context), softWrap: softWrap, overflow: overflow, textScaler: textScaler, maxLines: maxLines, strutStyle: strutStyle, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, locale: locale ?? Localizations.maybeLocaleOf(context), registrar: selectionRegistrar, selectionColor: selectionColor, primary: primary, onShowMore: onShowMore, ); } @override void updateRenderObject(BuildContext context, RenderParagraph renderObject) { assert(textDirection != null || debugCheckHasDirectionality(context)); renderObject ..text = (text: text, primary: primary) ..textAlign = textAlign ..textDirection = textDirection ?? Directionality.of(context) ..softWrap = softWrap ..overflow = overflow ..textScaler = textScaler ..maxLines = maxLines ..strutStyle = strutStyle ..textWidthBasis = textWidthBasis ..textHeightBehavior = textHeightBehavior ..locale = locale ?? Localizations.maybeLocaleOf(context) ..registrar = selectionRegistrar ..selectionColor = selectionColor ..onShowMore = onShowMore; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( EnumProperty( 'textAlign', textAlign, defaultValue: TextAlign.start, ), ) ..add( EnumProperty( 'textDirection', textDirection, defaultValue: null, ), ) ..add( FlagProperty( 'softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true, ), ) ..add( EnumProperty( 'overflow', overflow, defaultValue: TextOverflow.clip, ), ) ..add( DiagnosticsProperty( 'textScaler', textScaler, defaultValue: TextScaler.noScaling, ), ) ..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')) ..add( EnumProperty( 'textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent, ), ) ..add(StringProperty('text', text.toPlainText())) ..add( DiagnosticsProperty('locale', locale, defaultValue: null), ) ..add( DiagnosticsProperty( 'strutStyle', strutStyle, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'textHeightBehavior', textHeightBehavior, defaultValue: null, ), ); } } ================================================ FILE: lib/common/widgets/flutter/text/text.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'package:flutter/gestures.dart'; /// @docImport 'package:flutter/material.dart'; /// /// @docImport 'editable_text.dart'; /// @docImport 'gesture_detector.dart'; /// @docImport 'implicit_animations.dart'; /// @docImport 'transitions.dart'; /// @docImport 'widget_span.dart'; library; import 'dart:math'; import 'dart:ui' as ui show TextHeightBehavior; import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide Text, RichText; import 'package:flutter/rendering.dart' hide RenderParagraph; /// A run of text with a single style. /// /// The [Text] widget displays a string of text with single style. The string /// might break across multiple lines or might all be displayed on the same line /// depending on the layout constraints. /// /// The [style] argument is optional. When omitted, the text will use the style /// from the closest enclosing [DefaultTextStyle]. If the given style's /// [TextStyle.inherit] property is true (the default), the given style will /// be merged with the closest enclosing [DefaultTextStyle]. This merging /// behavior is useful, for example, to make the text bold while using the /// default font family and size. /// /// {@tool snippet} /// /// This example shows how to display text using the [Text] widget with the /// [overflow] set to [TextOverflow.ellipsis]. /// /// ![If the text overflows, the Text widget displays an ellipsis to trim the overflowing text](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_ellipsis.png) /// /// ```dart /// Container( /// width: 100, /// decoration: BoxDecoration(border: Border.all()), /// child: const Text( /// 'Hello, how are you?', /// overflow: TextOverflow.ellipsis, /// ), /// ) /// ``` /// {@end-tool} /// /// {@tool snippet} /// /// Setting [maxLines] to `1` is not equivalent to disabling soft wrapping with /// [softWrap]. This is apparent when using [TextOverflow.fade] as the following /// examples show. /// /// ![If a second line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_max_lines.png) /// /// ```dart /// const Text( /// 'Hello, how are you?', /// overflow: TextOverflow.fade, /// maxLines: 1, /// ) /// ``` /// /// Here soft wrapping is enabled and the [Text] widget tries to wrap the words /// "how are you?" to a second line. This is prevented by the [maxLines] value /// of `1`. The result is that a second line overflows and the fade appears in a /// horizontal direction at the bottom. /// /// ![If a single line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_soft_wrap.png) /// /// ```dart /// const Text( /// 'Hello, how are you?', /// overflow: TextOverflow.fade, /// softWrap: false, /// ) /// ``` /// /// Here soft wrapping is disabled with `softWrap: false` and the [Text] widget /// attempts to display its text in a single unbroken line. The result is that /// the single line overflows and the fade appears in a vertical direction at /// the right. /// /// {@end-tool} /// /// Using the [Text.rich] constructor, the [Text] widget can /// display a paragraph with differently styled [TextSpan]s. The sample /// that follows displays "Hello beautiful world" with different styles /// for each word. /// /// {@tool snippet} /// /// ![The word "Hello" is shown with the default text styles. The word "beautiful" is italicized. The word "world" is bold.](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_rich.png) /// /// ```dart /// const Text.rich( /// TextSpan( /// text: 'Hello', // default text style /// children: [ /// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), /// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), /// ], /// ), /// ) /// ``` /// {@end-tool} /// /// ## Interactivity /// /// To make [Text] react to touch events, wrap it in a [GestureDetector] widget /// with a [GestureDetector.onTap] handler. /// /// In a Material Design application, consider using a [TextButton] instead, or /// if that isn't appropriate, at least using an [InkWell] instead of /// [GestureDetector]. /// /// To make sections of the text interactive, use [RichText] and specify a /// [TapGestureRecognizer] as the [TextSpan.recognizer] of the relevant part of /// the text. /// /// ## Selection /// /// [Text] is not selectable by default. To make a [Text] selectable, one can /// wrap a subtree with a [SelectionArea] widget. To exclude a part of a subtree /// under [SelectionArea] from selection, once can also wrap that part of the /// subtree with [SelectionContainer.disabled]. /// /// {@tool dartpad} /// This sample demonstrates how to disable selection for a Text under a /// SelectionArea. /// /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** /// {@end-tool} /// /// See also: /// /// * [RichText], which gives you more control over the text styles. /// * [DefaultTextStyle], which sets default styles for [Text] widgets. /// * [SelectableRegion], which provides an overview of the selection system. class Text extends StatelessWidget { /// Creates a text widget. /// /// If the [style] argument is null, the text will use the style from the /// closest enclosing [DefaultTextStyle]. /// /// The [overflow] property's behavior is affected by the [softWrap] argument. /// If the [softWrap] is true or null, the glyph causing overflow, and those /// that follow, will not be rendered. Otherwise, it will be shown with the /// given overflow option. const Text( String this.data, { super.key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) this.textScaleFactor, this.textScaler, this.maxLines, this.semanticsLabel, this.semanticsIdentifier, this.textWidthBasis, this.textHeightBehavior, this.selectionColor, required this.primary, this.onShowMore, }) : textSpan = null, assert( textScaler == null || textScaleFactor == null, 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', ); /// Creates a text widget with a [InlineSpan]. /// /// The following subclasses of [InlineSpan] may be used to build rich text: /// /// * [TextSpan]s define text and children [InlineSpan]s. /// * [WidgetSpan]s define embedded inline widgets. /// /// See [RichText] which provides a lower-level way to draw text. const Text.rich( InlineSpan this.textSpan, { super.key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) this.textScaleFactor, this.textScaler, this.maxLines, this.semanticsLabel, this.semanticsIdentifier, this.textWidthBasis, this.textHeightBehavior, this.selectionColor, required this.primary, this.onShowMore, }) : data = null, assert( textScaler == null || textScaleFactor == null, 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', ); /// The text to display. /// /// This will be null if a [textSpan] is provided instead. final String? data; /// The text to display as a [InlineSpan]. /// /// This will be null if [data] is provided instead. final InlineSpan? textSpan; /// If non-null, the style to use for this text. /// /// If the style's "inherit" property is true, the style will be merged with /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will /// replace the closest enclosing [DefaultTextStyle]. /// /// The user or platform may override this [style]'s [TextStyle.fontWeight], /// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing] /// via a [MediaQuery] ancestor's [MediaQueryData.boldText], /// [MediaQueryData.lineHeightScaleFactorOverride], /// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride] /// regardless of its [TextStyle.inherit] value. final TextStyle? style; /// {@macro flutter.painting.textPainter.strutStyle} /// /// The user or platform may override this [strutStyle]'s [StrutStyle.height] /// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride]. final StrutStyle? strutStyle; /// How the text should be aligned horizontally. final TextAlign? textAlign; /// The directionality of the text. /// /// This decides how [textAlign] values like [TextAlign.start] and /// [TextAlign.end] are interpreted. /// /// This is also used to disambiguate how to render bidirectional text. For /// example, if the [data] is an English phrase followed by a Hebrew phrase, /// in a [TextDirection.ltr] context the English phrase will be on the left /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. /// /// Defaults to the ambient [Directionality], if any. final TextDirection? textDirection; /// Used to select a font when the same Unicode character can /// be rendered differently, depending on the locale. /// /// It's rarely necessary to set this property. By default its value /// is inherited from the enclosing app with `Localizations.localeOf(context)`. /// /// See [RenderParagraph.locale] for more information. final Locale? locale; /// Whether the text should break at soft line breaks. /// /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. final bool? softWrap; /// How visual overflow should be handled. /// /// If this is null [TextStyle.overflow] will be used, otherwise the value /// from the nearest [DefaultTextStyle] ancestor will be used. final TextOverflow? overflow; /// Deprecated. Will be removed in a future version of Flutter. Use /// [textScaler] instead. /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. /// /// The value given to the constructor as textScaleFactor. If null, will /// use the [MediaQueryData.textScaleFactor] obtained from the ambient /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) final double? textScaleFactor; /// {@macro flutter.painting.textPainter.textScaler} final TextScaler? textScaler; /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. /// /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the /// edge of the box. /// /// If this is null, but there is an ambient [DefaultTextStyle] that specifies /// an explicit number for its [DefaultTextStyle.maxLines], then the /// [DefaultTextStyle] value will take precedence. You can use a [RichText] /// widget directly to entirely override the [DefaultTextStyle]. final int? maxLines; /// {@template flutter.widgets.Text.semanticsLabel} /// An alternative semantics label for this text. /// /// If present, the semantics of this widget will contain this value instead /// of the actual text. This will overwrite any of the semantics labels applied /// directly to the [TextSpan]s. /// /// This is useful for replacing abbreviations or shorthands with the full /// text value: /// /// ```dart /// const Text(r'$$', semanticsLabel: 'Double dollars') /// ``` /// {@endtemplate} final String? semanticsLabel; /// A unique identifier for the semantics node for this widget. /// /// This is useful for cases where the text widget needs to have a uniquely /// identifiable ID that is recognized through the automation tools without /// having a dependency on the actual content of the text that can possibly be /// dynamic in nature. final String? semanticsIdentifier; /// {@macro flutter.painting.textPainter.textWidthBasis} final TextWidthBasis? textWidthBasis; /// {@macro dart.ui.textHeightBehavior} final ui.TextHeightBehavior? textHeightBehavior; /// The color to use when painting the selection. /// /// This is ignored if [SelectionContainer.maybeOf] returns null /// in the [BuildContext] of the [Text] widget. /// /// If null, the ambient [DefaultSelectionStyle] is used (if any); failing /// that, the selection color defaults to [DefaultSelectionStyle.defaultColor] /// (semi-transparent grey). final Color? selectionColor; final Color primary; final VoidCallback? onShowMore; @override Widget build(BuildContext context) { final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); TextStyle? effectiveTextStyle = style; if (style == null || style!.inherit) { effectiveTextStyle = defaultTextStyle.style.merge(style); } if (MediaQuery.boldTextOf(context)) { effectiveTextStyle = effectiveTextStyle!.merge( const TextStyle(fontWeight: FontWeight.bold), ); } // TODO(Renzo-Olivares): Investigate ways the framework can automatically // apply MediaQueryData.paragraphSpacingOverride to its own text components. // See: https://github.com/flutter/flutter/issues/177953 and https://github.com/flutter/flutter/issues/177408. final double? lineHeightScaleFactor = MediaQuery.maybeLineHeightScaleFactorOverrideOf(context); final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf( context, ); final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context); final TextSpan effectiveTextSpan = _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( lineHeightScaleFactor: lineHeightScaleFactor, letterSpacing: letterSpacing, wordSpacing: wordSpacing, textSpan: TextSpan( style: effectiveTextStyle, text: data, locale: locale, children: textSpan != null ? [textSpan!] : null, ), ); final StrutStyle? effectiveStrutStyle = strutStyle?.merge( StrutStyle(height: lineHeightScaleFactor), ); final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) { (final TextScaler textScaler, _) => textScaler, // For unmigrated apps, fall back to textScaleFactor. (null, final double textScaleFactor) => TextScaler.linear( textScaleFactor, ), (null, null) => MediaQuery.textScalerOf(context), }; late Widget result; if (registrar != null) { result = MouseRegion( cursor: DefaultSelectionStyle.of(context).mouseCursor ?? SystemMouseCursors.text, child: _SelectableTextContainer( textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null. locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null softWrap: softWrap ?? defaultTextStyle.softWrap, overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow, textScaler: textScaler, maxLines: maxLines ?? defaultTextStyle.maxLines, strutStyle: effectiveStrutStyle, textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), selectionColor: selectionColor ?? DefaultSelectionStyle.of(context).selectionColor ?? DefaultSelectionStyle.defaultColor, text: effectiveTextSpan, primary: primary, ), ); } else { result = RichText( textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null. locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null softWrap: softWrap ?? defaultTextStyle.softWrap, overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow, textScaler: textScaler, maxLines: maxLines ?? defaultTextStyle.maxLines, strutStyle: effectiveStrutStyle, textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), selectionColor: selectionColor ?? DefaultSelectionStyle.of(context).selectionColor ?? DefaultSelectionStyle.defaultColor, text: effectiveTextSpan, primary: primary, onShowMore: onShowMore, ); } if (semanticsLabel != null || semanticsIdentifier != null) { result = Semantics( textDirection: textDirection, label: semanticsLabel, identifier: semanticsIdentifier, child: ExcludeSemantics( excluding: semanticsLabel != null, child: result, ), ); } return result; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(StringProperty('data', data, showName: false)); if (textSpan != null) { properties.add( textSpan!.toDiagnosticsNode( name: 'textSpan', style: DiagnosticsTreeStyle.transition, ), ); } style?.debugFillProperties(properties); properties ..add( EnumProperty('textAlign', textAlign, defaultValue: null), ) ..add( EnumProperty( 'textDirection', textDirection, defaultValue: null, ), ) ..add( DiagnosticsProperty('locale', locale, defaultValue: null), ) ..add( FlagProperty( 'softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true, ), ) ..add( EnumProperty('overflow', overflow, defaultValue: null), ) ..add( DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null), ) ..add(IntProperty('maxLines', maxLines, defaultValue: null)) ..add( EnumProperty( 'textWidthBasis', textWidthBasis, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'textHeightBehavior', textHeightBehavior, defaultValue: null, ), ); if (semanticsLabel != null) { properties.add(StringProperty('semanticsLabel', semanticsLabel)); } if (semanticsIdentifier != null) { properties.add( StringProperty('semanticsIdentifier', semanticsIdentifier), ); } } } class _SelectableTextContainer extends StatefulWidget { const _SelectableTextContainer({ required this.text, required this.textAlign, this.textDirection, required this.softWrap, required this.overflow, required this.textScaler, this.maxLines, this.locale, this.strutStyle, required this.textWidthBasis, this.textHeightBehavior, required this.selectionColor, required this.primary, }); final TextSpan text; final TextAlign textAlign; final TextDirection? textDirection; final bool softWrap; final TextOverflow overflow; final TextScaler textScaler; final int? maxLines; final Locale? locale; final StrutStyle? strutStyle; final TextWidthBasis textWidthBasis; final ui.TextHeightBehavior? textHeightBehavior; final Color selectionColor; final Color primary; @override State<_SelectableTextContainer> createState() => _SelectableTextContainerState(); } class _SelectableTextContainerState extends State<_SelectableTextContainer> { late final _SelectableTextContainerDelegate _selectionDelegate; final GlobalKey _textKey = GlobalKey(); @override void initState() { super.initState(); _selectionDelegate = _SelectableTextContainerDelegate(_textKey); } @override void dispose() { _selectionDelegate.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SelectionContainer( delegate: _selectionDelegate, // Use [_RichText] wrapper so the underlying [RenderParagraph] can register // its [Selectable]s to the [SelectionContainer] created by this widget. child: _RichText( textKey: _textKey, textAlign: widget.textAlign, textDirection: widget.textDirection, locale: widget.locale, softWrap: widget.softWrap, overflow: widget.overflow, textScaler: widget.textScaler, maxLines: widget.maxLines, strutStyle: widget.strutStyle, textWidthBasis: widget.textWidthBasis, textHeightBehavior: widget.textHeightBehavior, selectionColor: widget.selectionColor, text: widget.text, primary: widget.primary, ), ); } } class _RichText extends StatelessWidget { const _RichText({ this.textKey, required this.text, required this.textAlign, this.textDirection, required this.softWrap, required this.overflow, required this.textScaler, this.maxLines, this.locale, this.strutStyle, required this.textWidthBasis, this.textHeightBehavior, required this.selectionColor, required this.primary, }); final GlobalKey? textKey; final InlineSpan text; final TextAlign textAlign; final TextDirection? textDirection; final bool softWrap; final TextOverflow overflow; final TextScaler textScaler; final int? maxLines; final Locale? locale; final StrutStyle? strutStyle; final TextWidthBasis textWidthBasis; final ui.TextHeightBehavior? textHeightBehavior; final Color selectionColor; final Color primary; @override Widget build(BuildContext context) { final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); return RichText( key: textKey, textAlign: textAlign, textDirection: textDirection, locale: locale, softWrap: softWrap, overflow: overflow, textScaler: textScaler, maxLines: maxLines, strutStyle: strutStyle, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, selectionRegistrar: registrar, selectionColor: selectionColor, text: text, primary: primary, ); } } // In practice some selectables like widgetspan shift several pixels. So when // the vertical position diff is within the threshold, compare the horizontal // position to make the compareScreenOrder function more robust. const double _kSelectableVerticalComparingThreshold = 3.0; class _SelectableTextContainerDelegate extends StaticSelectionContainerDelegate { _SelectableTextContainerDelegate(GlobalKey textKey) : _textKey = textKey; final GlobalKey _textKey; RenderParagraph get paragraph => _textKey.currentContext!.findRenderObject()! as RenderParagraph; @override SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) { final SelectionResult result = _handleSelectParagraph(event); super.didReceiveSelectionBoundaryEvents(); return result; } SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) { if (event.absorb) { for (var index = 0; index < selectables.length; index += 1) { dispatchSelectionEventToChild(selectables[index], event); } currentSelectionStartIndex = 0; currentSelectionEndIndex = selectables.length - 1; return SelectionResult.next; } // First pass, if the position is on a placeholder then dispatch the selection // event to the [Selectable] at the location and terminate. for (var index = 0; index < selectables.length; index += 1) { final bool selectableIsPlaceholder = !paragraph .selectableBelongsToParagraph(selectables[index]); if (selectableIsPlaceholder && selectables[index].boundingBoxes.isNotEmpty) { for (final Rect rect in selectables[index].boundingBoxes) { final Rect globalRect = MatrixUtils.transformRect( selectables[index].getTransformTo(null), rect, ); if (globalRect.contains(event.globalPosition)) { currentSelectionStartIndex = currentSelectionEndIndex = index; return dispatchSelectionEventToChild(selectables[index], event); } } } } SelectionResult? lastSelectionResult; var foundStart = false; int? lastNextIndex; for (var index = 0; index < selectables.length; index += 1) { if (!paragraph.selectableBelongsToParagraph(selectables[index])) { if (foundStart) { final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( globalPosition: event.globalPosition, absorb: true, ); final SelectionResult result = dispatchSelectionEventToChild( selectables[index], synthesizedEvent, ); if (selectables.length - 1 == index) { currentSelectionEndIndex = index; _flushInactiveSelections(); return result; } } continue; } final SelectionGeometry existingGeometry = selectables[index].value; lastSelectionResult = dispatchSelectionEventToChild( selectables[index], event, ); if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) { if (foundStart) { currentSelectionEndIndex = index; } else { currentSelectionStartIndex = currentSelectionEndIndex = index; } return SelectionResult.next; } if (lastSelectionResult == SelectionResult.next) { if (selectables[index].value == existingGeometry && !foundStart) { lastNextIndex = index; } if (selectables[index].value != existingGeometry && !foundStart) { assert(selectables[index].boundingBoxes.isNotEmpty); assert(selectables[index].value.selectionRects.isNotEmpty); final bool selectionAtStartOfSelectable = selectables[index] .boundingBoxes[0] .overlaps( selectables[index].value.selectionRects[0], ); var startIndex = 0; if (lastNextIndex != null && selectionAtStartOfSelectable) { startIndex = lastNextIndex + 1; } else { startIndex = lastNextIndex == null && selectionAtStartOfSelectable ? 0 : index; } for (var i = startIndex; i < index; i += 1) { final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( globalPosition: event.globalPosition, absorb: true, ); dispatchSelectionEventToChild(selectables[i], synthesizedEvent); } currentSelectionStartIndex = startIndex; foundStart = true; } continue; } if (index == 0 && lastSelectionResult == SelectionResult.previous) { return SelectionResult.previous; } if (selectables[index].value != existingGeometry) { if (!foundStart && lastNextIndex == null) { currentSelectionStartIndex = 0; for (var i = 0; i < index; i += 1) { final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( globalPosition: event.globalPosition, absorb: true, ); dispatchSelectionEventToChild(selectables[i], synthesizedEvent); } } currentSelectionEndIndex = index; // Geometry has changed as a result of select paragraph, need to clear the // selection of other selectables to keep selection in sync. _flushInactiveSelections(); } return SelectionResult.end; } assert(lastSelectionResult == null); return SelectionResult.end; } /// Initializes the selection of the selectable children. /// /// The goal is to find the selectable child that contains the selection edge. /// Returns [SelectionResult.end] if the selection edge ends on any of the /// children. Otherwise, it returns [SelectionResult.previous] if the selection /// does not reach any of its children. Returns [SelectionResult.next] /// if the selection reaches the end of its children. /// /// Ideally, this method should only be called twice at the beginning of the /// drag selection, once for start edge update event, once for end edge update /// event. SelectionResult _initSelection( SelectionEdgeUpdateEvent event, { required bool isEnd, }) { assert( (isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1), ); SelectionResult? finalResult; // Begin the search for the selection edge at the opposite edge if it exists. final hasOppositeEdge = isEnd ? currentSelectionStartIndex != -1 : currentSelectionEndIndex != -1; int newIndex = switch ((isEnd, hasOppositeEdge)) { (true, true) => currentSelectionStartIndex, (true, false) => 0, (false, true) => currentSelectionEndIndex, (false, false) => 0, }; bool? forward; late SelectionResult currentSelectableResult; // This loop sends the selection event to one of the following to determine // the direction of the search. // - The opposite edge index if it exists. // - Index 0 if the opposite edge index does not exist. // // If the result is `SelectionResult.next`, this loop look backward. // Otherwise, it looks forward. // // The terminate condition are: // 1. the selectable returns end, pending, none. // 2. the selectable returns previous when looking forward. // 2. the selectable returns next when looking backward. while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) { currentSelectableResult = dispatchSelectionEventToChild( selectables[newIndex], event, ); switch (currentSelectableResult) { case SelectionResult.end: case SelectionResult.pending: case SelectionResult.none: finalResult = currentSelectableResult; case SelectionResult.next: if (forward == false) { newIndex += 1; finalResult = SelectionResult.end; } else if (newIndex == selectables.length - 1) { finalResult = currentSelectableResult; } else { forward = true; newIndex += 1; } case SelectionResult.previous: if (forward ?? false) { newIndex -= 1; finalResult = SelectionResult.end; } else if (newIndex == 0) { finalResult = currentSelectableResult; } else { forward = false; newIndex -= 1; } } } if (isEnd) { currentSelectionEndIndex = newIndex; } else { currentSelectionStartIndex = newIndex; } _flushInactiveSelections(); return finalResult!; } SelectionResult _adjustSelection( SelectionEdgeUpdateEvent event, { required bool isEnd, }) { assert(() { if (isEnd) { assert( currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0, ); return true; } assert( currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0, ); return true; }()); SelectionResult? finalResult; // Determines if the edge being adjusted is within the current viewport. // - If so, we begin the search for the new selection edge position at the // currentSelectionEndIndex/currentSelectionStartIndex. // - If not, we attempt to locate the new selection edge starting from // the opposite end. // - If neither edge is in the current viewport, the search for the new // selection edge position begins at 0. // // This can happen when there is a scrollable child and the edge being adjusted // has been scrolled out of view. final isCurrentEdgeWithinViewport = isEnd ? value.endSelectionPoint != null : value.startSelectionPoint != null; final isOppositeEdgeWithinViewport = isEnd ? value.startSelectionPoint != null : value.endSelectionPoint != null; int newIndex = switch (( isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport, )) { (true, true, true) => currentSelectionEndIndex, (true, true, false) => currentSelectionEndIndex, (true, false, true) => currentSelectionStartIndex, (true, false, false) => 0, (false, true, true) => currentSelectionStartIndex, (false, true, false) => currentSelectionStartIndex, (false, false, true) => currentSelectionEndIndex, (false, false, false) => 0, }; bool? forward; late SelectionResult currentSelectableResult; // This loop sends the selection event to one of the following to determine // the direction of the search. // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge // is in the current viewport. // - The opposite edge index if the current edge is not in the current viewport. // - Index 0 if neither edge is in the current viewport. // // If the result is `SelectionResult.next`, this loop look backward. // Otherwise, it looks forward. // // The terminate condition are: // 1. the selectable returns end, pending, none. // 2. the selectable returns previous when looking forward. // 2. the selectable returns next when looking backward. while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) { currentSelectableResult = dispatchSelectionEventToChild( selectables[newIndex], event, ); switch (currentSelectableResult) { case SelectionResult.end: case SelectionResult.pending: case SelectionResult.none: finalResult = currentSelectableResult; case SelectionResult.next: if (forward == false) { newIndex += 1; finalResult = SelectionResult.end; } else if (newIndex == selectables.length - 1) { finalResult = currentSelectableResult; } else { forward = true; newIndex += 1; } case SelectionResult.previous: if (forward ?? false) { newIndex -= 1; finalResult = SelectionResult.end; } else if (newIndex == 0) { finalResult = currentSelectableResult; } else { forward = false; newIndex -= 1; } } } if (isEnd) { final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex; if (forward != null && ((!forwardSelection && forward && newIndex >= currentSelectionStartIndex) || (forwardSelection && !forward && newIndex <= currentSelectionStartIndex))) { currentSelectionStartIndex = currentSelectionEndIndex; } currentSelectionEndIndex = newIndex; } else { final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex; if (forward != null && ((!forwardSelection && !forward && newIndex <= currentSelectionEndIndex) || (forwardSelection && forward && newIndex >= currentSelectionEndIndex))) { currentSelectionEndIndex = currentSelectionStartIndex; } currentSelectionStartIndex = newIndex; } _flushInactiveSelections(); return finalResult!; } /// The compare function this delegate used for determining the selection /// order of the [Selectable]s. /// /// Sorts the [Selectable]s by their top left [Rect]. @override Comparator get compareOrder => _compareScreenOrder; static int _compareScreenOrder(Selectable a, Selectable b) { // Attempt to sort the selectables under a [_SelectableTextContainerDelegate] // by the top left rect. final Rect rectA = MatrixUtils.transformRect( a.getTransformTo(null), a.boundingBoxes.first, ); final Rect rectB = MatrixUtils.transformRect( b.getTransformTo(null), b.boundingBoxes.first, ); final int result = _compareVertically(rectA, rectB); if (result != 0) { return result; } return _compareHorizontally(rectA, rectB); } /// Compares two rectangles in the screen order solely by their vertical /// positions. /// /// Returns positive if a is lower, negative if a is higher, 0 if their /// order can't be determine solely by their vertical position. static int _compareVertically(Rect a, Rect b) { // The rectangles overlap so defer to horizontal comparison. if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) || (b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) { return 0; } if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) { return a.top > b.top ? 1 : -1; } return a.bottom > b.bottom ? 1 : -1; } /// Compares two rectangles in the screen order by their horizontal positions /// assuming one of the rectangles enclose the other rect vertically. /// /// Returns positive if a is lower, negative if a is higher. static int _compareHorizontally(Rect a, Rect b) { // a encloses b. if (a.left - b.left < precisionErrorTolerance && a.right - b.right > -precisionErrorTolerance) { return -1; } // b encloses a. if (b.left - a.left < precisionErrorTolerance && b.right - a.right > -precisionErrorTolerance) { return 1; } if ((a.left - b.left).abs() > precisionErrorTolerance) { return a.left > b.left ? 1 : -1; } return a.right > b.right ? 1 : -1; } /// This method calculates a local [SelectedContentRange] based on the list /// of [selections] that are accumulated from the [Selectable] children under this /// delegate. This calculation takes into account the accumulated content /// length before the active selection, and returns null when either selection /// edge has not been set. SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) { if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { return null; } var startOffset = 0; var endOffset = 0; var foundStart = false; bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex; if (currentSelectionEndIndex == currentSelectionStartIndex) { // Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex. // Use the range from the selectable within the selection as the source of truth for selection direction. final SelectedContentRange rangeAtSelectableInSelection = selectables[currentSelectionStartIndex].getSelection()!; forwardSelection = rangeAtSelectableInSelection.endOffset >= rangeAtSelectableInSelection.startOffset; } for (var index = 0; index < selections.length; index++) { final _SelectionInfo selection = selections[index]; if (selection.range == null) { if (foundStart) { return SelectedContentRange( startOffset: forwardSelection ? startOffset : endOffset, endOffset: forwardSelection ? endOffset : startOffset, ); } startOffset += selection.contentLength; endOffset = startOffset; continue; } final int selectionStartNormalized = min( selection.range!.startOffset, selection.range!.endOffset, ); final int selectionEndNormalized = max( selection.range!.startOffset, selection.range!.endOffset, ); if (!foundStart) { // Because a RenderParagraph may split its content into multiple selectables // we have to consider at what offset a selectable starts at relative // to the RenderParagraph, when the selectable is not the start of the content. final bool shouldConsiderContentStart = index > 0 && paragraph.selectableBelongsToParagraph(selectables[index]); startOffset += (selectionStartNormalized - (shouldConsiderContentStart ? paragraph .getPositionForOffset( selectables[index] .boundingBoxes .first .centerLeft, ) .offset : 0)) .abs(); endOffset = startOffset + (selectionEndNormalized - selectionStartNormalized).abs(); foundStart = true; } else { endOffset += (selectionEndNormalized - selectionStartNormalized).abs(); } } assert( foundStart, 'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.', ); return SelectedContentRange( startOffset: forwardSelection ? startOffset : endOffset, endOffset: forwardSelection ? endOffset : startOffset, ); } /// Returns a [SelectedContentRange] considering the [SelectedContentRange] /// from each [Selectable] child managed under this delegate. /// /// When nothing is selected or either selection edge has not been set, /// this method will return `null`. @override SelectedContentRange? getSelection() { final selections = <_SelectionInfo>[ for (final Selectable selectable in selectables) ( contentLength: selectable.contentLength, range: selectable.getSelection(), ), ]; return _calculateLocalRange(selections); } // From [SelectableRegion]. // Clears the selection on all selectables not in the range of // currentSelectionStartIndex..currentSelectionEndIndex. // // If one of the edges does not exist, then this method will clear the selection // in all selectables except the existing edge. // // If neither of the edges exist this method immediately returns. void _flushInactiveSelections() { if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) { return; } if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { final int skipIndex = currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex; selectables .where((Selectable target) => target != selectables[skipIndex]) .forEach( (Selectable target) => dispatchSelectionEventToChild( target, const ClearSelectionEvent(), ), ); return; } final int skipStart = min( currentSelectionStartIndex, currentSelectionEndIndex, ); final int skipEnd = max( currentSelectionStartIndex, currentSelectionEndIndex, ); for (var index = 0; index < selectables.length; index += 1) { if (index >= skipStart && index <= skipEnd) { continue; } dispatchSelectionEventToChild( selectables[index], const ClearSelectionEvent(), ); } } @override SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { if (event.granularity != TextGranularity.paragraph) { return super.handleSelectionEdgeUpdate(event); } updateLastSelectionEdgeLocation( globalSelectionEdgeLocation: event.globalPosition, forEnd: event.type == SelectionEventType.endEdgeUpdate, ); if (event.type == SelectionEventType.endEdgeUpdate) { return currentSelectionEndIndex == -1 ? _initSelection(event, isEnd: true) : _adjustSelection(event, isEnd: true); } return currentSelectionStartIndex == -1 ? _initSelection(event, isEnd: false) : _adjustSelection(event, isEnd: false); } } /// The length of the content that can be selected, and the range that is /// selected. typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range}); /// A utility class for overriding the text styles of a [TextSpan] tree. // When changes are made to this class, the equivalent API in editable_text.dart // must also be updated. // TODO(Renzo-Olivares): Remove after investigating a solution for overriding all // styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952. class _OverridingTextStyleTextSpanUtils { static TextSpan applyTextSpacingOverrides({ double? lineHeightScaleFactor, double? letterSpacing, double? wordSpacing, required TextSpan textSpan, }) { if (lineHeightScaleFactor == null && letterSpacing == null && wordSpacing == null) { return textSpan; } return _applyTextStyleOverrides( TextStyle( height: lineHeightScaleFactor, letterSpacing: letterSpacing, wordSpacing: wordSpacing, ), textSpan, ); } static TextSpan _applyTextStyleOverrides( TextStyle overrideTextStyle, TextSpan textSpan, ) { return TextSpan( text: textSpan.text, children: textSpan.children?.map((InlineSpan child) { if (child is TextSpan && child.runtimeType == TextSpan) { return _applyTextStyleOverrides(overrideTextStyle, child); } return child; }).toList(), style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle, recognizer: textSpan.recognizer, mouseCursor: textSpan.mouseCursor, onEnter: textSpan.onEnter, onExit: textSpan.onExit, semanticsLabel: textSpan.semanticsLabel, semanticsIdentifier: textSpan.semanticsIdentifier, locale: textSpan.locale, spellOut: textSpan.spellOut, ); } } ================================================ FILE: lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'selectable_text.dart'; /// @docImport 'selection_area.dart'; /// @docImport 'text_field.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; import 'package:flutter/material.dart' hide EditableText, EditableTextState; import 'package:flutter/rendering.dart'; /// The default context menu for text selection for the current platform. /// /// {@template flutter.material.AdaptiveTextSelectionToolbar.contextMenuBuilders} /// Typically, this widget would be passed to `contextMenuBuilder` in a /// supported parent widget, such as: /// /// * [EditableText.contextMenuBuilder] /// * [TextField.contextMenuBuilder] /// * [CupertinoTextField.contextMenuBuilder] /// * [SelectionArea.contextMenuBuilder] /// * [SelectableText.contextMenuBuilder] /// {@endtemplate} /// /// See also: /// /// * [EditableText.getEditableButtonItems], which returns the default /// [ContextMenuButtonItem]s for [EditableText] on the platform. /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button /// Widgets for the current platform given [ContextMenuButtonItem]s. /// * [CupertinoAdaptiveTextSelectionToolbar], which does the same thing as this /// widget but only for Cupertino context menus. /// * [TextSelectionToolbar], the default toolbar for Android. /// * [DesktopTextSelectionToolbar], the default toolbar for desktop platforms /// other than MacOS. /// * [CupertinoTextSelectionToolbar], the default toolbar for iOS. /// * [CupertinoDesktopTextSelectionToolbar], the default toolbar for MacOS. class AdaptiveTextSelectionToolbar extends StatelessWidget { /// Create an instance of [AdaptiveTextSelectionToolbar] with the /// given [children]. /// /// See also: /// /// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems} /// * [AdaptiveTextSelectionToolbar.buttonItems], which takes a list of /// [ContextMenuButtonItem]s instead of [children] widgets. /// {@endtemplate} /// {@template flutter.material.AdaptiveTextSelectionToolbar.editable} /// * [AdaptiveTextSelectionToolbar.editable], which builds the default /// children for an editable field. /// {@endtemplate} /// {@template flutter.material.AdaptiveTextSelectionToolbar.editableText} /// * [AdaptiveTextSelectionToolbar.editableText], which builds the default /// children for an [EditableText]. /// {@endtemplate} /// {@template flutter.material.AdaptiveTextSelectionToolbar.selectable} /// * [AdaptiveTextSelectionToolbar.selectable], which builds the default /// children for content that is selectable but not editable. /// {@endtemplate} const AdaptiveTextSelectionToolbar({ super.key, required this.children, required this.anchors, }) : buttonItems = null; /// Create an instance of [AdaptiveTextSelectionToolbar] whose children will /// be built from the given [buttonItems]. /// /// See also: /// /// {@template flutter.material.AdaptiveTextSelectionToolbar.new} /// * [AdaptiveTextSelectionToolbar.new], which takes the children directly as /// a list of widgets. /// {@endtemplate} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable} const AdaptiveTextSelectionToolbar.buttonItems({ super.key, required this.buttonItems, required this.anchors, }) : children = null; /// Create an instance of [AdaptiveTextSelectionToolbar] with the default /// children for an editable field. /// /// If an on* callback parameter is null, then its corresponding button will /// not be built. /// /// These callbacks are called when their corresponding button is activated /// and only then. For example, `onPaste` is called when the user taps the /// "Paste" button in the context menu and not when the user pastes with the /// keyboard. /// /// See also: /// /// {@macro flutter.material.AdaptiveTextSelectionToolbar.new} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable} AdaptiveTextSelectionToolbar.editable({ super.key, required ClipboardStatus clipboardStatus, required VoidCallback? onCopy, required VoidCallback? onCut, required VoidCallback? onPaste, required VoidCallback? onSelectAll, required VoidCallback? onLookUp, required VoidCallback? onSearchWeb, required VoidCallback? onShare, required VoidCallback? onLiveTextInput, required this.anchors, }) : children = null, buttonItems = EditableText.getEditableButtonItems( clipboardStatus: clipboardStatus, onCopy: onCopy, onCut: onCut, onPaste: onPaste, onSelectAll: onSelectAll, onLookUp: onLookUp, onSearchWeb: onSearchWeb, onShare: onShare, onLiveTextInput: onLiveTextInput, ); /// Create an instance of [AdaptiveTextSelectionToolbar] with the default /// children for an [EditableText]. /// /// See also: /// /// {@macro flutter.material.AdaptiveTextSelectionToolbar.new} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable} AdaptiveTextSelectionToolbar.editableText({ super.key, required EditableTextState editableTextState, }) : children = null, buttonItems = editableTextState.contextMenuButtonItems, anchors = editableTextState.contextMenuAnchors; /// Create an instance of [AdaptiveTextSelectionToolbar] with the default /// children for selectable, but not editable, content. /// /// See also: /// /// {@macro flutter.material.AdaptiveTextSelectionToolbar.new} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText} AdaptiveTextSelectionToolbar.selectable({ super.key, required VoidCallback onCopy, required VoidCallback onSelectAll, required VoidCallback? onShare, required SelectionGeometry selectionGeometry, required this.anchors, }) : children = null, buttonItems = SelectableRegion.getSelectableButtonItems( selectionGeometry: selectionGeometry, onCopy: onCopy, onSelectAll: onSelectAll, onShare: onShare, ); /// Create an instance of [AdaptiveTextSelectionToolbar] with the default /// children for a [SelectableRegion]. /// /// See also: /// /// {@macro flutter.material.AdaptiveTextSelectionToolbar.new} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText} /// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable} AdaptiveTextSelectionToolbar.selectableRegion({ super.key, required SelectableRegionState selectableRegionState, }) : children = null, buttonItems = selectableRegionState.contextMenuButtonItems, anchors = selectableRegionState.contextMenuAnchors; /// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems} /// The [ContextMenuButtonItem]s that will be turned into the correct button /// widgets for the current platform. /// {@endtemplate} final List? buttonItems; /// The children of the toolbar, typically buttons. final List? children; /// {@template flutter.material.AdaptiveTextSelectionToolbar.anchors} /// The location on which to anchor the menu. /// {@endtemplate} final TextSelectionToolbarAnchors anchors; /// Returns the default button label String for the button of the given /// [ContextMenuButtonType] on any platform. static String getButtonLabel( BuildContext context, ContextMenuButtonItem buttonItem, ) { if (buttonItem.label != null) { return buttonItem.label!; } switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: return CupertinoTextSelectionToolbarButton.getButtonLabel( context, buttonItem, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: assert(debugCheckHasMaterialLocalizations(context)); final MaterialLocalizations localizations = MaterialLocalizations.of( context, ); return switch (buttonItem.type) { ContextMenuButtonType.cut => localizations.cutButtonLabel, ContextMenuButtonType.copy => localizations.copyButtonLabel, ContextMenuButtonType.paste => localizations.pasteButtonLabel, ContextMenuButtonType.selectAll => localizations.selectAllButtonLabel, ContextMenuButtonType.delete => localizations.deleteButtonTooltip.toUpperCase(), ContextMenuButtonType.lookUp => localizations.lookUpButtonLabel, ContextMenuButtonType.searchWeb => localizations.searchWebButtonLabel, ContextMenuButtonType.share => localizations.shareButtonLabel, ContextMenuButtonType.liveTextInput => localizations.scanTextButtonLabel, ContextMenuButtonType.custom => '', }; } } /// Returns a List of Widgets generated by turning [buttonItems] into the /// default context menu buttons for the current platform. /// /// This is useful when building a text selection toolbar with the default /// button appearance for the given platform, but where the toolbar and/or the /// button actions and labels may be custom. /// /// {@tool dartpad} /// This sample demonstrates how to use `getAdaptiveButtons` to generate /// default button widgets in a custom toolbar. /// /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart ** /// {@end-tool} /// /// See also: /// /// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the /// Cupertino equivalent of this class and builds only the Cupertino /// buttons. static Iterable getAdaptiveButtons( BuildContext context, List buttonItems, ) { switch (Theme.of(context).platform) { case TargetPlatform.iOS: return buttonItems.map((ContextMenuButtonItem buttonItem) { return CupertinoTextSelectionToolbarButton.buttonItem( buttonItem: buttonItem, ); }); case TargetPlatform.fuchsia: case TargetPlatform.android: final buttons = []; for (var i = 0; i < buttonItems.length; i++) { final ContextMenuButtonItem buttonItem = buttonItems[i]; buttons.add( TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.getPadding( i, buttonItems.length, ), onPressed: buttonItem.onPressed, alignment: AlignmentDirectional.centerStart, child: Text(getButtonLabel(context, buttonItem)), ), ); } return buttons; case TargetPlatform.linux: case TargetPlatform.windows: return buttonItems.map((ContextMenuButtonItem buttonItem) { return DesktopTextSelectionToolbarButton.text( context: context, onPressed: buttonItem.onPressed, text: getButtonLabel(context, buttonItem), ); }); case TargetPlatform.macOS: return buttonItems.map((ContextMenuButtonItem buttonItem) { return CupertinoDesktopTextSelectionToolbarButton.text( onPressed: buttonItem.onPressed, text: getButtonLabel(context, buttonItem), ); }); } } @override Widget build(BuildContext context) { // If there aren't any buttons to build, build an empty toolbar. if ((children ?? buttonItems)?.isEmpty ?? true) { return const SizedBox.shrink(); } final List resultChildren = children != null ? children! : getAdaptiveButtons(context, buttonItems!).toList(); switch (Theme.of(context).platform) { case TargetPlatform.iOS: return CupertinoTextSelectionToolbar( anchorAbove: anchors.primaryAnchor, anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!, children: resultChildren, ); case TargetPlatform.android: return TextSelectionToolbar( anchorAbove: anchors.primaryAnchor, anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!, children: resultChildren, ); case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return DesktopTextSelectionToolbar( anchor: anchors.primaryAnchor, children: resultChildren, ); case TargetPlatform.macOS: return CupertinoDesktopTextSelectionToolbar( anchor: anchors.primaryAnchor, children: resultChildren, ); } } } ================================================ FILE: lib/common/widgets/flutter/text_field/controller.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// /// created by bggRGjQaUbCoE on 2025/6/27 /// enum RichTextType { text, composing, at, emoji, vote, common } class Emote { late String url; late double width; late double height; Emote({ required this.url, required this.width, double? height, }) : height = height ?? width; } mixin RichTextTypeMixin { RichTextType get type; Emote? get emote; String? get id; String? get rawText; } extension TextEditingDeltaExt on TextEditingDelta { ({RichTextType type, String? rawText, Emote? emote, String? id}) get config { if (this case final RichTextTypeMixin e) { return (type: e.type, rawText: e.rawText, emote: e.emote, id: e.id); } return ( type: composing.isValid ? RichTextType.composing : RichTextType.text, rawText: null, emote: null, id: null, ); } bool get isText { if (this case final RichTextTypeMixin e) { return e.type == RichTextType.text; } return !composing.isValid; } bool get isComposing { return composing.isValid; } } class RichTextEditingDeltaInsertion extends TextEditingDeltaInsertion with RichTextTypeMixin { RichTextEditingDeltaInsertion({ required super.oldText, required super.textInserted, required super.insertionOffset, required super.selection, required super.composing, RichTextType? type, this.emote, this.id, this.rawText, }) : type = type ?? (composing.isValid ? RichTextType.composing : RichTextType.text); @override late final RichTextType type; @override final Emote? emote; @override final String? id; @override final String? rawText; } class RichTextEditingDeltaReplacement extends TextEditingDeltaReplacement with RichTextTypeMixin { RichTextEditingDeltaReplacement({ required super.oldText, required super.replacementText, required super.replacedRange, required super.selection, required super.composing, RichTextType? type, this.emote, this.id, this.rawText, }) : type = type ?? (composing.isValid ? RichTextType.composing : RichTextType.text); @override late final RichTextType type; @override final Emote? emote; @override final String? id; @override final String? rawText; } class RichTextItem { late RichTextType type; late String text; String? _rawText; late TextRange range; Emote? emote; String? id; String get rawText => _rawText ?? text; bool get isText => type == RichTextType.text; bool get isComposing => type == RichTextType.composing; bool get isRich => !isText && !isComposing; RichTextItem({ this.type = RichTextType.text, required this.text, String? rawText, required this.range, this.emote, this.id, }) : _rawText = rawText; RichTextItem.fromStart( this.text, { String? rawText, this.type = RichTextType.text, this.emote, this.id, }) : range = TextRange(start: 0, end: text.length), _rawText = rawText; List? onInsert( TextEditingDeltaInsertion delta, RichTextEditingController controller, ) { final int insertionOffset = delta.insertionOffset; if (range.end < insertionOffset) { return null; } if (insertionOffset == 0 && range.start == 0) { final insertedLength = delta.textInserted.length; controller.newSelection = TextSelection.collapsed(offset: insertedLength); if (!isRich && delta.isText) { text = delta.textInserted + text; range = TextRange(start: range.start, end: range.start + text.length); return null; } range = TextRange( start: range.start + insertedLength, end: range.end + insertedLength, ); final config = delta.config; final insertedItem = RichTextItem.fromStart( delta.textInserted, rawText: config.rawText, type: config.type, emote: config.emote, id: config.id, ); return [insertedItem]; } if (range.start >= insertionOffset) { final int insertedLength = delta.textInserted.length; range = TextRange( start: range.start + insertedLength, end: range.end + insertedLength, ); return null; } if (range.end == insertionOffset) { final end = insertionOffset + delta.textInserted.length; controller.newSelection = TextSelection.collapsed(offset: end); if ((isText && delta.isText) || (isComposing && delta.isComposing)) { text += delta.textInserted; range = TextRange(start: range.start, end: end); return null; } final config = delta.config; final insertedItem = RichTextItem( type: config.type, emote: config.emote, id: config.id, text: delta.textInserted, rawText: config.rawText, range: TextRange(start: insertionOffset, end: end), ); return [insertedItem]; } if (!isRich && range.start < insertionOffset && range.end > insertionOffset) { final leadingText = text.substring(0, insertionOffset - range.start); final trailingString = text.substring(leadingText.length); final insertEnd = insertionOffset + delta.textInserted.length; controller.newSelection = TextSelection.collapsed(offset: insertEnd); if (delta.isText) { text = leadingText + delta.textInserted + trailingString; range = TextRange( start: range.start, end: range.start + text.length, ); return null; } final config = delta.config; final insertedItem = RichTextItem( type: config.type, emote: config.emote, id: config.id, text: delta.textInserted, rawText: config.rawText, range: TextRange(start: insertionOffset, end: insertEnd), ); final trailItem = RichTextItem( text: trailingString, range: TextRange( start: insertEnd, end: insertEnd + trailingString.length, ), ); text = leadingText; range = TextRange( start: range.start, end: range.start + leadingText.length, ); return [insertedItem, trailItem]; } return null; } ({bool remove, bool cal})? onDelete( TextEditingDeltaDeletion delta, RichTextEditingController controller, int? delLength, ) { final deletedRange = delta.deletedRange; if (range.end <= deletedRange.start) { return null; } if (range.start >= deletedRange.end) { final length = delLength ?? delta.textDeleted.length; range = TextRange( start: range.start - length, end: range.end - length, ); return null; } if (range.start < deletedRange.start && range.end > deletedRange.end) { if (isRich) { controller.newSelection = TextSelection.collapsed(offset: range.start); return (remove: true, cal: true); } text = text.replaceRange( deletedRange.start - range.start, deletedRange.end - range.start, '', ); range = TextRange(start: range.start, end: range.start + text.length); controller.newSelection = TextSelection.collapsed( offset: deletedRange.start, ); return null; } if (range.start >= deletedRange.start && range.end <= deletedRange.end) { if (range.start == deletedRange.start) { controller.newSelection = TextSelection.collapsed(offset: range.start); } return (remove: true, cal: false); } if (range.start < deletedRange.start && range.end <= deletedRange.end) { if (isRich) { controller.newSelection = TextSelection.collapsed(offset: range.start); return (remove: true, cal: true); } text = text.replaceRange( text.length - (range.end - deletedRange.start), null, '', ); range = TextRange( start: range.start, end: deletedRange.start, ); controller.newSelection = TextSelection.collapsed( offset: deletedRange.start, ); return null; } if (range.start >= deletedRange.start && range.end > deletedRange.end) { final start = min(deletedRange.start, range.start); controller.newSelection = TextSelection.collapsed(offset: start); if (isRich) { return (remove: true, cal: true); } text = text.substring(deletedRange.end - range.start); range = TextRange( start: start, end: start + text.length, ); return null; } return null; } ({bool remove, List? toAdd})? onReplace( TextEditingDeltaReplacement delta, RichTextEditingController controller, ) { final replacedRange = delta.replacedRange; if (range.end <= replacedRange.start) { return null; } if (range.start >= replacedRange.end) { final before = replacedRange.end - replacedRange.start; final after = delta.replacementText.length; final length = after - before; range = TextRange( start: range.start + length, end: range.end + length, ); return null; } if (range.start < replacedRange.start && range.end > replacedRange.end) { if (!isRich) { if (delta.isText) { text = text.replaceRange( replacedRange.start - range.start, replacedRange.end - range.start, delta.replacementText, ); final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed( offset: replacedRange.start + delta.replacementText.length, ); return null; } else { final leadingText = text.substring( 0, replacedRange.start - range.start, ); final trailString = text.substring(replacedRange.end - range.start); final insertEnd = replacedRange.start + delta.replacementText.length; controller.newSelection = TextSelection.collapsed(offset: insertEnd); final config = delta.config; final insertedItem = RichTextItem( type: config.type, emote: config.emote, id: config.id, text: delta.replacementText, rawText: config.rawText, range: TextRange( start: replacedRange.start, end: insertEnd, ), ); final trailItem = RichTextItem( text: trailString, range: TextRange( start: insertEnd, end: insertEnd + trailString.length, ), ); text = leadingText; range = TextRange( start: range.start, end: range.start + leadingText.length, ); return ( remove: false, toAdd: [insertedItem, trailItem], ); } } final config = delta.config; text = delta.replacementText; type = config.type; emote = config.emote; id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed(offset: end); return null; } if (range.start >= replacedRange.start && range.end <= replacedRange.end) { if (range.start == replacedRange.start) { text = delta.replacementText; final config = delta.config; _rawText = config.rawText; type = config.type; emote = config.emote; id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed(offset: end); return (remove: false, toAdd: null); } return (remove: true, toAdd: null); } if (range.start < replacedRange.start && range.end <= replacedRange.end) { if (!isRich) { if (delta.isText) { text = text.replaceRange( text.length - (range.end - replacedRange.start), null, delta.replacementText, ); final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed(offset: end); return null; } else { text = text.replaceRange( text.length - (range.end - replacedRange.start), null, '', ); range = TextRange(start: range.start, end: range.start + text.length); final end = replacedRange.start + delta.replacementText.length; final config = delta.config; final insertedItem = RichTextItem( text: delta.replacementText, rawText: config.rawText, type: config.type, emote: config.emote, id: config.id, range: TextRange(start: replacedRange.start, end: end), ); controller.newSelection = TextSelection.collapsed(offset: end); return (remove: false, toAdd: [insertedItem]); } } text = delta.replacementText; final config = delta.config; type = config.type; emote = config.emote; id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed(offset: end); return null; } if (range.start >= replacedRange.start && range.end > replacedRange.end) { if (range.start > replacedRange.start) { if (!isRich) { text = text.substring(replacedRange.end - range.start); final start = replacedRange.start + delta.replacementText.length; range = TextRange(start: start, end: start + text.length); return null; } return (remove: true, toAdd: null); } if (!isRich) { if (delta.isText) { text = text.replaceRange( 0, replacedRange.end - range.start, delta.replacementText, ); final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed(offset: end); return null; } else { final end = range.start + delta.replacementText.length; final config = delta.config; final insertedItem = RichTextItem( text: delta.replacementText, rawText: config.rawText, type: config.type, emote: config.emote, id: config.id, range: TextRange(start: range.start, end: end), ); controller.newSelection = TextSelection.collapsed(offset: end); text = text.substring(replacedRange.end - range.start); range = TextRange(start: end, end: end + text.length); return (remove: true, toAdd: [insertedItem]); } } text = delta.replacementText; final config = delta.config; type = config.type; emote = config.emote; id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); controller.newSelection = TextSelection.collapsed(offset: end); return null; } return null; } @override String toString() { return '\ntype: [${type.name}],' 'text: [$text],' 'rawText: [$_rawText],' '\nrange: [TextRange(start: ${range.start}, end: ${range.end})]\n'; } } class RichTextEditingController extends TextEditingController { RichTextEditingController({ List? items, this.onMention, }) : super( text: items != null && items.isNotEmpty ? (StringBuffer()..writeAll(items.map((e) => e.text))).toString() : null, ) { if (items != null && items.isNotEmpty) { this.items.addAll(items); } } final VoidCallback? onMention; TextSelection newSelection = const TextSelection.collapsed(offset: 0); final List items = []; String get plainText { if (items.isEmpty) { return ''; } final buffer = StringBuffer(); for (final e in items) { buffer.write(e.text); } return buffer.toString(); } String get rawText { if (items.isEmpty) { return ''; } final buffer = StringBuffer(); for (final e in items) { if (e.type == RichTextType.at) { buffer.write(e.text); } else { buffer.write(e.rawText); } } return buffer.toString(); } void syncRichText(TextEditingDelta delta) { int? addIndex; List? toAdd; int? delLength; List? toDel; switch (delta) { case TextEditingDeltaInsertion e: if (e.textInserted == '@') { onMention?.call(); } if (items.isEmpty) { final config = delta.config; items.add( RichTextItem.fromStart( delta.textInserted, rawText: config.rawText, type: config.type, emote: config.emote, id: config.id, ), ); newSelection = TextSelection.collapsed( offset: delta.textInserted.length, ); return; } for (int index = 0; index < items.length; index++) { List? newItems = items[index].onInsert(e, this); if (newItems != null) { addIndex = (e.insertionOffset == 0 && index == 0) ? 0 : index + 1; toAdd = newItems; } } case TextEditingDeltaDeletion e: for (int index = 0; index < items.length; index++) { final item = items[index]; ({bool remove, bool cal})? res = item.onDelete(e, this, delLength); if (res != null) { if (res.remove) { (toDel ??= []).add(item); } if (res.cal) { delLength ??= item.text.length; } } } case TextEditingDeltaReplacement e: for (int index = 0; index < items.length; index++) { final item = items[index]; ({bool remove, List? toAdd})? res = item.onReplace( e, this, ); if (res != null) { if (res.toAdd != null) { addIndex = res.remove ? index : (e.replacedRange.start == 0 && index == 0) ? 0 : index + 1; (toAdd ??= []).addAll(res.toAdd!); } else if (res.remove) { (toDel ??= []).add(item); } } } case TextEditingDeltaNonTextUpdate e: newSelection = e.selection; if (newSelection.isCollapsed) { final newPos = dragOffset(newSelection.base); newSelection = newSelection.copyWith( baseOffset: newPos.offset, extentOffset: newPos.offset, ); } else { final isNormalized = newSelection.baseOffset < newSelection.extentOffset; var startOffset = newSelection.start; var endOffset = newSelection.end; final newOffset = longPressOffset(startOffset, endOffset); startOffset = newOffset.startOffset; endOffset = newOffset.endOffset; newSelection = newSelection.copyWith( baseOffset: isNormalized ? startOffset : endOffset, extentOffset: isNormalized ? endOffset : startOffset, ); } } if (addIndex != null && toAdd != null && toAdd.isNotEmpty) { items.insertAll(addIndex, toAdd); } if (toDel != null && toDel.isNotEmpty) { for (final item in toDel) { items.remove(item); } } } TextStyle? composingStyle; TextStyle? richStyle; @override TextSpan buildTextSpan({ required BuildContext context, TextStyle? style, required bool withComposing, }) { assert( !value.composing.isValid || !withComposing || value.isComposingRangeValid, ); final bool composingRegionOutOfRange = !value.isComposingRangeValid || !withComposing; // if (composingRegionOutOfRange) { // return TextSpan(style: style, text: text); // } // bool isValid = true; // int cursor = 0; // for (final e in items) { // final range = e.range; // if (range.start == cursor) { // cursor = range.end; // } else { // isValid = false; // break; // } // } // debugPrint('isValid: $isValid,,${text.length},,${plainText.length}'); // debugPrint('$items\n$selection'); return TextSpan( style: style, children: items.map((e) { switch (e.type) { case RichTextType.text: return TextSpan(text: e.text); case RichTextType.composing: composingStyle ??= style?.merge( const TextStyle(decoration: TextDecoration.underline), ) ?? const TextStyle(decoration: TextDecoration.underline); if (composingRegionOutOfRange) { e.type = RichTextType.text; } return TextSpan( text: e.text, style: composingRegionOutOfRange ? null : composingStyle, ); case RichTextType.at || RichTextType.common: richStyle ??= (style ?? const TextStyle()).copyWith( color: Theme.of(context).colorScheme.primary, ); return TextSpan( text: e.text, style: richStyle, ); case RichTextType.emoji: final emote = e.emote; if (emote != null) { return WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: NetworkImgLayer( src: emote.url, width: 22, // emote.width, height: 22, // emote.height, type: ImageType.emote, fit: BoxFit.contain, ), ), ); } return TextSpan(text: e.text); case RichTextType.vote: richStyle ??= (style ?? const TextStyle()).copyWith( color: Theme.of(context).colorScheme.primary, ); return TextSpan( children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( Icons.bar_chart_rounded, size: 22, color: richStyle!.color, ), ), TextSpan( text: '${e.rawText} ', style: richStyle, ), ], ); } }).toList(), ); // final TextStyle composingStyle = // style?.merge(const TextStyle(decoration: TextDecoration.underline)) ?? // const TextStyle(decoration: TextDecoration.underline); // return TextSpan( // style: style, // children: [ // TextSpan(text: value.composing.textBefore(value.text)), // TextSpan( // style: composingStyle, // text: value.composing.textInside(value.text)), // TextSpan(text: value.composing.textAfter(value.text)), // ], // ); } @override void clear() { items.clear(); super.clear(); } @override void dispose() { items.clear(); super.dispose(); } TextPosition dragOffset(TextPosition position) { final offset = position.offset; for (final e in items) { final range = e.range; if (offset >= range.end) { continue; } if (offset <= range.start) { break; } if (e.isRich) { if (offset * 2 > range.start + range.end) { return TextPosition(offset: range.end); } else { return TextPosition(offset: range.start); } } } return position; } int tapOffsetSimple(int offset) { for (final e in items) { final range = e.range; if (offset >= range.end) { continue; } if (offset <= range.start) { break; } if (e.isRich) { if (offset * 2 > range.start + range.end) { return range.end; } else { return range.start; } } } return offset; } int tapOffset( int offset, { required TextPainter textPainter, required Offset localPos, required Offset lastTapDownPosition, }) { for (final e in items) { final range = e.range; if (offset >= range.end) { continue; } if (offset < range.start) { break; } // emoji tap if (offset == range.start) { if (e.emote != null) { final closestOffset = textPainter.getClosestGlyphForOffset(localPos); if (closestOffset != null) { final offsetRect = closestOffset.graphemeClusterLayoutBounds; final offsetRange = closestOffset.graphemeClusterCodeUnitRange; if (lastTapDownPosition.dx > offsetRect.right) { return offsetRange.end; } else { return offsetRange.start; } } } } else { if (e.isRich) { if (offset * 2 > range.start + range.end) { return range.end; } else { return range.start; } } } } return offset; } ({int startOffset, int endOffset}) longPressOffset( int startOffset, int endOffset, ) { for (final e in items) { final range = e.range; if (startOffset >= range.end) { continue; } if (endOffset <= range.start) { break; } late final cal = range.start + range.end; if (startOffset > range.start && startOffset < range.end) { if (e.isRich) { if (startOffset * 2 > cal) { startOffset = range.end; } else { startOffset = range.start; } } } if (endOffset > range.start && endOffset < range.end) { if (e.isRich) { if (endOffset * 2 > cal) { endOffset = range.end; } else { endOffset = range.start; } } } } return (startOffset: startOffset, endOffset: endOffset); } TextSelection keyboardOffset(TextSelection newSelection) { final offset = newSelection.baseOffset; for (final e in items) { final range = e.range; if (offset >= range.end) { continue; } if (offset <= range.start) { break; } if (offset > range.start && offset < range.end) { if (e.isRich) { if (offset < selection.baseOffset) { return newSelection.copyWith( baseOffset: range.start, extentOffset: range.start, ); } else { return newSelection.copyWith( baseOffset: range.end, extentOffset: range.end, ); } } } } return newSelection; } TextSelection keyboardOffsets(TextSelection newSelection) { final startOffset = newSelection.start; final endOffset = newSelection.end; final isNormalized = newSelection.baseOffset < newSelection.extentOffset; for (final e in items) { final range = e.range; if (startOffset >= range.end) { continue; } if (endOffset <= range.start) { break; } if (isNormalized) { if (startOffset <= range.start && endOffset > range.start && endOffset < range.end) { if (e.isRich) { if (endOffset < selection.extentOffset) { return newSelection.copyWith( baseOffset: startOffset, extentOffset: range.start, ); } else { return newSelection.copyWith( baseOffset: startOffset, extentOffset: range.end, ); } } } } else { if (startOffset < range.end && startOffset > range.start) { if (e.isRich) { if (startOffset > selection.extentOffset) { return newSelection.copyWith( baseOffset: endOffset, extentOffset: range.end, ); } else { return newSelection.copyWith( baseOffset: endOffset, extentOffset: range.start, ); } } } } } return newSelection; } String? getSelectionText(TextSelection selection) { try { String text = ''; final start = selection.start; final end = selection.end; for (final e in items) { final range = e.range; if (start >= range.end) { continue; } if (end <= range.start) { break; } if (e.isRich) { if (e.emote != null) { text += e.rawText; } else { text += e.text; } } else { text += e.text.substring( max(start, range.start) - range.start, min(end, range.end) - range.start, ); } } return text; } catch (e) { if (kDebugMode) debugPrint('err getSelectionText: $e'); return null; } } } ================================================ FILE: lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/material.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/rendering.dart'; /// The default Cupertino context menu for text selection for the current /// platform with the given children. /// /// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.platforms} /// Builds the mobile Cupertino context menu on all mobile platforms, not just /// iOS, and builds the desktop Cupertino context menu on all desktop platforms, /// not just MacOS. For a widget that builds the native-looking context menu for /// all platforms, see [AdaptiveTextSelectionToolbar]. /// {@endtemplate} /// /// See also: /// /// * [AdaptiveTextSelectionToolbar], which does the same thing as this widget /// but for all platforms, not just the Cupertino-styled platforms. /// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds /// the Cupertino button Widgets for the current platform given /// [ContextMenuButtonItem]s. class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget { /// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the /// given [children]. /// /// See also: /// /// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems} /// * [CupertinoAdaptiveTextSelectionToolbar.buttonItems], which takes a list /// of [ContextMenuButtonItem]s instead of [children] widgets. /// {@endtemplate} /// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable} /// * [CupertinoAdaptiveTextSelectionToolbar.editable], which builds the /// default Cupertino children for an editable field. /// {@endtemplate} /// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText} /// * [CupertinoAdaptiveTextSelectionToolbar.editableText], which builds the /// default Cupertino children for an [EditableText]. /// {@endtemplate} /// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable} /// * [CupertinoAdaptiveTextSelectionToolbar.selectable], which builds the /// Cupertino children for content that is selectable but not editable. /// {@endtemplate} const CupertinoAdaptiveTextSelectionToolbar({ super.key, required this.children, required this.anchors, }) : buttonItems = null; /// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] whose /// children will be built from the given [buttonItems]. /// /// See also: /// /// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new} /// * [CupertinoAdaptiveTextSelectionToolbar.new], which takes the children /// directly as a list of widgets. /// {@endtemplate} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable} const CupertinoAdaptiveTextSelectionToolbar.buttonItems({ super.key, required this.buttonItems, required this.anchors, }) : children = null; /// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the /// default children for an editable field. /// /// If a callback is null, then its corresponding button will not be built. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.editable], which is similar to this but /// includes Material and Cupertino toolbars. /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable} CupertinoAdaptiveTextSelectionToolbar.editable({ super.key, required ClipboardStatus clipboardStatus, required VoidCallback? onCopy, required VoidCallback? onCut, required VoidCallback? onPaste, required VoidCallback? onSelectAll, required VoidCallback? onLookUp, required VoidCallback? onSearchWeb, required VoidCallback? onShare, required VoidCallback? onLiveTextInput, required this.anchors, }) : children = null, buttonItems = EditableText.getEditableButtonItems( clipboardStatus: clipboardStatus, onCopy: onCopy, onCut: onCut, onPaste: onPaste, onSelectAll: onSelectAll, onLookUp: onLookUp, onSearchWeb: onSearchWeb, onShare: onShare, onLiveTextInput: onLiveTextInput, ); /// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the /// default children for an [EditableText]. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.editableText], which is similar to this /// but includes Material and Cupertino toolbars. /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable} CupertinoAdaptiveTextSelectionToolbar.editableText({ super.key, required EditableTextState editableTextState, }) : children = null, buttonItems = editableTextState.contextMenuButtonItems, anchors = editableTextState.contextMenuAnchors; /// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the /// default children for selectable, but not editable, content. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.selectable], which is similar to this but /// includes Material and Cupertino toolbars. /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable} /// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText} CupertinoAdaptiveTextSelectionToolbar.selectable({ super.key, required VoidCallback onCopy, required VoidCallback onSelectAll, required SelectionGeometry selectionGeometry, required this.anchors, }) : children = null, buttonItems = SelectableRegion.getSelectableButtonItems( selectionGeometry: selectionGeometry, onCopy: onCopy, onSelectAll: onSelectAll, onShare: null, // See https://github.com/flutter/flutter/issues/141775. ); /// {@macro flutter.material.AdaptiveTextSelectionToolbar.anchors} final TextSelectionToolbarAnchors anchors; /// The children of the toolbar, typically buttons. final List? children; /// The [ContextMenuButtonItem]s that will be turned into the correct button /// widgets for the current platform. final List? buttonItems; /// Returns a List of Widgets generated by turning [buttonItems] into the /// default context menu buttons for Cupertino on the current platform. /// /// This is useful when building a text selection toolbar with the default /// button appearance for the given platform, but where the toolbar and/or the /// button actions and labels may be custom. /// /// Does not build Material buttons. On non-Apple platforms, Cupertino buttons /// will still be used, because the Cupertino library does not access the /// Material library. To get the native-looking buttons on every platform, /// use [AdaptiveTextSelectionToolbar.getAdaptiveButtons] in the Material /// library. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the Material /// equivalent of this class and builds only the Material buttons. It /// includes a live example of using `getAdaptiveButtons`. static Iterable getAdaptiveButtons( BuildContext context, List buttonItems, ) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: return buttonItems.map((ContextMenuButtonItem buttonItem) { return CupertinoTextSelectionToolbarButton.buttonItem( buttonItem: buttonItem, ); }); case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.macOS: return buttonItems.map((ContextMenuButtonItem buttonItem) { return CupertinoDesktopTextSelectionToolbarButton.buttonItem( buttonItem: buttonItem, ); }); } } @override Widget build(BuildContext context) { // If there aren't any buttons to build, build an empty toolbar. if ((children ?? buttonItems)?.isEmpty ?? true) { return const SizedBox.shrink(); } final List resultChildren = children ?? getAdaptiveButtons(context, buttonItems!).toList(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: case TargetPlatform.fuchsia: return CupertinoTextSelectionToolbar( anchorAbove: anchors.primaryAnchor, anchorBelow: anchors.secondaryAnchor ?? anchors.primaryAnchor, children: resultChildren, ); case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.macOS: return CupertinoDesktopTextSelectionToolbar( anchor: anchors.primaryAnchor, children: resultChildren, ); } } } ================================================ FILE: lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/material.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan; /// iOS only shows 3 spell check suggestions in the toolbar. const int _kMaxSuggestions = 3; /// The default spell check suggestions toolbar for iOS. /// /// Tries to position itself below the [anchors], but if it doesn't fit, then it /// readjusts to fit above bottom view insets. /// /// See also: /// * [SpellCheckSuggestionsToolbar], which is similar but for both the /// Material and Cupertino libraries. class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget { /// Constructs a [CupertinoSpellCheckSuggestionsToolbar]. /// /// [buttonItems] must not contain more than three items. const CupertinoSpellCheckSuggestionsToolbar({ super.key, required this.anchors, required this.buttonItems, }) : assert(buttonItems.length <= _kMaxSuggestions); /// Constructs a [CupertinoSpellCheckSuggestionsToolbar] with the default /// children for an [EditableText]. /// /// See also: /// * [SpellCheckSuggestionsToolbar.editableText], which is similar but /// builds an Android-style toolbar. CupertinoSpellCheckSuggestionsToolbar.editableText({ super.key, required EditableTextState editableTextState, }) : buttonItems = buildButtonItems(editableTextState) ?? [], anchors = editableTextState.contextMenuAnchors; /// The location on which to anchor the menu. final TextSelectionToolbarAnchors anchors; /// The [ContextMenuButtonItem]s that will be turned into the correct button /// widgets and displayed in the spell check suggestions toolbar. /// /// Must not contain more than three items. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.buttonItems], the list of /// [ContextMenuButtonItem]s that are used to build the buttons of the /// text selection toolbar. /// * [SpellCheckSuggestionsToolbar.buttonItems], the list of /// [ContextMenuButtonItem]s used to build the Material style spell check /// suggestions toolbar. final List buttonItems; /// Builds the button items for the toolbar based on the available /// spell check suggestions. static List? buildButtonItems( EditableTextState editableTextState, ) { // Determine if composing region is misspelled. final SuggestionSpan? spanAtCursorIndex = editableTextState .findSuggestionSpanAtCursorIndex( editableTextState.currentTextEditingValue.selection.baseOffset, ); if (spanAtCursorIndex == null) { return null; } if (spanAtCursorIndex.suggestions.isEmpty) { assert(debugCheckHasCupertinoLocalizations(editableTextState.context)); final CupertinoLocalizations localizations = CupertinoLocalizations.of( editableTextState.context, ); return [ ContextMenuButtonItem( onPressed: null, label: localizations.noSpellCheckReplacementsLabel, ), ]; } final buttonItems = []; // Build suggestion buttons. for (final String suggestion in spanAtCursorIndex.suggestions.take( _kMaxSuggestions, )) { buttonItems.add( ContextMenuButtonItem( onPressed: () { if (!editableTextState.mounted) { return; } _replaceText( editableTextState, suggestion, spanAtCursorIndex.range, ); }, label: suggestion, ), ); } return buttonItems; } static void _replaceText( EditableTextState editableTextState, String text, TextRange replacementRange, ) { // Replacement cannot be performed if the text is read only or obscured. assert( !editableTextState.widget.readOnly && !editableTextState.widget.obscureText, ); final TextEditingValue newValue = editableTextState.textEditingValue .replaced(replacementRange, text) .copyWith( selection: TextSelection.collapsed( offset: replacementRange.start + text.length, ), ); editableTextState.userUpdateTextEditingValue( newValue, SelectionChangedCause.toolbar, ); // Schedule a call to bringIntoView() after renderEditable updates. SchedulerBinding.instance.addPostFrameCallback((Duration duration) { if (editableTextState.mounted) { editableTextState.bringIntoView( editableTextState.textEditingValue.selection.extent, ); } }, debugLabel: 'SpellCheckSuggestions.bringIntoView'); editableTextState.hideToolbar(); } /// Builds the toolbar buttons based on the [buttonItems]. List _buildToolbarButtons(BuildContext context) { return buttonItems.map((ContextMenuButtonItem buttonItem) { return CupertinoTextSelectionToolbarButton.buttonItem( buttonItem: buttonItem, ); }).toList(); } @override Widget build(BuildContext context) { if (buttonItems.isEmpty) { return const SizedBox.shrink(); } final List children = _buildToolbarButtons(context); return CupertinoTextSelectionToolbar( anchorAbove: anchors.primaryAnchor, anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!, children: children, ); } } ================================================ FILE: lib/common/widgets/flutter/text_field/cupertino/text_field.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/material.dart'; library; import 'dart:math' as math; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/system_context_menu.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState, CupertinoSpellCheckSuggestionsToolbar, EditableTextContextMenuBuilder, SystemContextMenu, CupertinoAdaptiveTextSelectionToolbar, SpellCheckConfiguration, TextSelectionGestureDetectorBuilderDelegate, TextSelectionGestureDetectorBuilder, TextSelectionOverlay; import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; const TextStyle _kDefaultPlaceholderStyle = TextStyle( fontWeight: FontWeight.w400, color: CupertinoColors.placeholderText, ); // Value inspected from Xcode 11 & iOS 13.0 Simulator. const BorderSide _kDefaultRoundedBorderSide = BorderSide( color: CupertinoDynamicColor.withBrightness( color: Color(0x33000000), darkColor: Color(0x33FFFFFF), ), width: 0.0, ); const Border _kDefaultRoundedBorder = Border( top: _kDefaultRoundedBorderSide, bottom: _kDefaultRoundedBorderSide, left: _kDefaultRoundedBorderSide, right: _kDefaultRoundedBorderSide, ); const BoxDecoration _kDefaultRoundedBorderDecoration = BoxDecoration( color: CupertinoDynamicColor.withBrightness( color: CupertinoColors.white, darkColor: CupertinoColors.black, ), border: _kDefaultRoundedBorder, borderRadius: BorderRadius.all(Radius.circular(5.0)), ); const Color _kDisabledBackground = CupertinoDynamicColor.withBrightness( color: Color(0xFFFAFAFA), darkColor: Color(0xFF050505), ); // Value inspected from Xcode 12 & iOS 14.0 Simulator. // Note it may not be consistent with https://developer.apple.com/design/resources/. const CupertinoDynamicColor _kClearButtonColor = CupertinoDynamicColor.withBrightness( color: Color(0x33000000), darkColor: Color(0x33FFFFFF), ); // An eyeballed value that moves the cursor slightly left of where it is // rendered for text on Android so it's positioning more accurately matches the // native iOS text cursor positioning. // // This value is in device pixels, not logical pixels as is typically used // throughout the codebase. const int _iOSHorizontalCursorOffsetPixels = -2; class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { _CupertinoTextFieldSelectionGestureDetectorBuilder({ required _CupertinoRichTextFieldState state, required super.controller, }) : _state = state, super(delegate: state); final _CupertinoRichTextFieldState _state; @override void onSingleTapUp(TapDragUpDetails details) { // Because TextSelectionGestureDetector listens to taps that happen on // widgets in front of it, tapping the clear button will also trigger // this handler. If the clear button widget recognizes the up event, // then do not handle it. if (_state._clearGlobalKey.currentContext != null) { final renderBox = _state._clearGlobalKey.currentContext!.findRenderObject()! as RenderBox; final Offset localOffset = renderBox.globalToLocal( details.globalPosition, ); if (renderBox.hitTest(BoxHitTestResult(), position: localOffset)) { return; } } super.onSingleTapUp(details); _state.widget.onTap?.call(); } @override void onDragSelectionEnd(TapDragEndDetails details) { _state._requestKeyboard(); super.onDragSelectionEnd(details); } } /// An iOS-style text field. /// /// A text field lets the user enter text, either with a hardware keyboard or with /// an onscreen keyboard. /// /// This widget corresponds to both a `UITextField` and an editable `UITextView` /// on iOS. /// /// The text field calls the [onChanged] callback whenever the user changes the /// text in the field. If the user indicates that they are done typing in the /// field (e.g., by pressing a button on the soft keyboard), the text field /// calls the [onSubmitted] callback. /// /// {@macro flutter.widgets.EditableText.onChanged} /// /// {@tool dartpad} /// This example shows how to set the initial value of the [CupertinoRichTextField] using /// a [controller] that already contains some text. /// /// ** See code in examples/api/lib/cupertino/text_field/cupertino_text_field.0.dart ** /// {@end-tool} /// /// The [controller] can also control the selection and composing region (and to /// observe changes to the text, selection, and composing region). /// /// The text field has an overridable [decoration] that, by default, draws a /// rounded rectangle border around the text field. If you set the [decoration] /// property to null, the decoration will be removed entirely. /// /// {@macro flutter.material.textfield.wantKeepAlive} /// /// Remember to call [RichTextEditingController.dispose] when it is no longer /// needed. This will ensure we discard any resources used by the object. /// /// {@macro flutter.widgets.editableText.showCaretOnScreen} /// /// ## Scrolling Considerations /// /// If this [CupertinoRichTextField] is not a descendant of [Scaffold] and is being /// used within a [Scrollable] or nested [Scrollable]s, consider placing a /// [ScrollNotificationObserver] above the root [Scrollable] that contains this /// [CupertinoRichTextField] to ensure proper scroll coordination for /// [CupertinoRichTextField] and its components like [TextSelectionOverlay]. /// /// See also: /// /// * /// * [TextField], an alternative text field widget that follows the Material /// Design UI conventions. /// * [EditableText], which is the raw text editing control at the heart of a /// [TextField]. /// * Learn how to use a [RichTextEditingController] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller). /// * class CupertinoRichTextField extends StatefulWidget { /// Creates an iOS-style text field. /// /// To provide a prefilled text entry, pass in a [RichTextEditingController] with /// an initial value to the [controller] parameter. /// /// To provide a hint placeholder text that appears when the text entry is /// empty, pass a [String] to the [placeholder] parameter. /// /// The [maxLines] property can be set to null to remove the restriction on /// the number of lines. In this mode, the intrinsic height of the widget will /// grow as the number of lines of text grows. By default, it is `1`, meaning /// this is a single-line text field and will scroll horizontally when /// it overflows. [maxLines] must not be zero. /// /// The text cursor is not shown if [showCursor] is false or if [showCursor] /// is null (the default) and [readOnly] is true. /// /// If specified, the [maxLength] property must be greater than zero. /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default /// to [EditableText.defaultSelectionHeightStyle] and /// [EditableText.defaultSelectionWidthStyle], respectively. /// /// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior], /// [expands], [obscureText], [prefixMode], [readOnly], [scrollPadding], /// [suffixMode], [textAlign], [selectionHeightStyle], [selectionWidthStyle], /// [enableSuggestions], and [enableIMEPersonalizedLearning] properties must /// not be null. /// /// {@macro flutter.widgets.editableText.accessibility} /// /// See also: /// /// * [minLines], which is the minimum number of lines to occupy when the /// content spans fewer lines. /// * [expands], to allow the widget to size itself to its parent's height. /// * [maxLength], which discusses the precise meaning of "number of /// characters" and how it may differ from the intuitive meaning. const CupertinoRichTextField({ super.key, this.groupId = EditableText, required this.controller, this.focusNode, this.decoration = _kDefaultRoundedBorderDecoration, this.padding = const EdgeInsets.all(7.0), this.placeholder, this.placeholderStyle = const TextStyle( fontWeight: FontWeight.w400, color: CupertinoColors.placeholderText, ), this.prefix, this.prefixMode = OverlayVisibilityMode.always, this.suffix, this.suffixMode = OverlayVisibilityMode.always, this.crossAxisAlignment = CrossAxisAlignment.center, this.clearButtonMode = OverlayVisibilityMode.never, this.clearButtonSemanticLabel, TextInputType? keyboardType, this.textInputAction, this.textCapitalization = TextCapitalization.none, this.style, this.strutStyle, this.textAlign = TextAlign.start, this.textAlignVertical, this.textDirection, this.readOnly = false, @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.toolbarOptions, this.showCursor, this.autofocus = false, this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect = true, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, this.maxLines = 1, this.minLines, this.expands = false, this.maxLength, this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, this.onTapOutside, this.onTapUpOutside, this.inputFormatters, this.enabled = true, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius = const Radius.circular(2.0), this.cursorOpacityAnimates = true, this.cursorColor, this.selectionHeightStyle, this.selectionWidthStyle, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, this.selectAllOnFocus, this.selectionControls, this.onTap, this.scrollController, this.scrollPhysics, this.autofillHints = const [], this.contentInsertionConfiguration, this.clipBehavior = Clip.hardEdge, this.restorationId, @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) this.scribbleEnabled = true, this.stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, this.contextMenuBuilder = _defaultContextMenuBuilder, this.spellCheckConfiguration, this.magnifierConfiguration, }) : assert(obscuringCharacter.length == 1), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( !expands || (maxLines == null && minLines == null), 'minLines and maxLines must be null when expands is true.', ), assert( !obscureText || maxLines == 1, 'Obscured fields cannot be multiline.', ), assert(maxLength == null || maxLength > 0), // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set. assert( !identical(textInputAction, TextInputAction.newline) || maxLines == 1 || !identical(keyboardType, TextInputType.text), 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.', ), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText); /// Creates a borderless iOS-style text field. /// /// To provide a prefilled text entry, pass in a [RichTextEditingController] with /// an initial value to the [controller] parameter. /// /// To provide a hint placeholder text that appears when the text entry is /// empty, pass a [String] to the [placeholder] parameter. /// /// The [maxLines] property can be set to null to remove the restriction on /// the number of lines. In this mode, the intrinsic height of the widget will /// grow as the number of lines of text grows. By default, it is `1`, meaning /// this is a single-line text field and will scroll horizontally when /// it overflows. [maxLines] must not be zero. /// /// The text cursor is not shown if [showCursor] is false or if [showCursor] /// is null (the default) and [readOnly] is true. /// /// If specified, the [maxLength] property must be greater than zero. /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively. /// /// See also: /// /// * [minLines], which is the minimum number of lines to occupy when the /// content spans fewer lines. /// * [expands], to allow the widget to size itself to its parent's height. /// * [maxLength], which discusses the precise meaning of "number of /// characters" and how it may differ from the intuitive meaning. const CupertinoRichTextField.borderless({ super.key, this.groupId = EditableText, required this.controller, this.focusNode, this.decoration, this.padding = const EdgeInsets.all(7.0), this.placeholder, this.placeholderStyle = _kDefaultPlaceholderStyle, this.prefix, this.prefixMode = OverlayVisibilityMode.always, this.suffix, this.suffixMode = OverlayVisibilityMode.always, this.crossAxisAlignment = CrossAxisAlignment.center, this.clearButtonMode = OverlayVisibilityMode.never, this.clearButtonSemanticLabel, TextInputType? keyboardType, this.textInputAction, this.textCapitalization = TextCapitalization.none, this.style, this.strutStyle, this.textAlign = TextAlign.start, this.textAlignVertical, this.textDirection, this.readOnly = false, @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.toolbarOptions, this.showCursor, this.autofocus = false, this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, this.maxLines = 1, this.minLines, this.expands = false, this.maxLength, this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, this.onTapOutside, this.onTapUpOutside, this.inputFormatters, this.enabled = true, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius = const Radius.circular(2.0), this.cursorOpacityAnimates = true, this.cursorColor, this.selectionHeightStyle, this.selectionWidthStyle, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, this.selectAllOnFocus, this.selectionControls, this.onTap, this.scrollController, this.scrollPhysics, this.autofillHints = const [], this.contentInsertionConfiguration, this.clipBehavior = Clip.hardEdge, this.restorationId, @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) this.scribbleEnabled = true, this.stylusHandwritingEnabled = true, this.enableIMEPersonalizedLearning = true, this.contextMenuBuilder = _defaultContextMenuBuilder, this.spellCheckConfiguration, this.magnifierConfiguration, }) : assert(obscuringCharacter.length == 1), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( !expands || (maxLines == null && minLines == null), 'minLines and maxLines must be null when expands is true.', ), assert( !obscureText || maxLines == 1, 'Obscured fields cannot be multiline.', ), assert(maxLength == null || maxLength > 0), // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set. assert( !identical(textInputAction, TextInputAction.newline) || maxLines == 1 || !identical(keyboardType, TextInputType.text), 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.', ), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText); /// {@macro flutter.widgets.editableText.groupId} final Object groupId; /// Controls the text being edited. /// /// If null, this widget will create its own [RichTextEditingController]. final RichTextEditingController controller; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// Controls the [BoxDecoration] of the box behind the text input. /// /// Defaults to having a rounded rectangle grey border and can be null to have /// no box decoration. final BoxDecoration? decoration; /// Padding around the text entry area between the [prefix] and [suffix] /// or the clear button when [clearButtonMode] is not never. /// /// Defaults to a padding of 6 pixels on all sides and can be null. final EdgeInsetsGeometry padding; /// A lighter colored placeholder hint that appears on the first line of the /// text field when the text entry is empty. /// /// Defaults to having no placeholder text. /// /// The text style of the placeholder text matches that of the text field's /// main text entry except a lighter font weight and a grey font color. final String? placeholder; /// The style to use for the placeholder text. /// /// The [placeholderStyle] is merged with the [style] [TextStyle] when applied /// to the [placeholder] text. To avoid merging with [style], specify /// [TextStyle.inherit] as false. /// /// Defaults to the [style] property with w300 font weight and grey color. /// /// If specifically set to null, placeholder's style will be the same as [style]. final TextStyle? placeholderStyle; /// An optional [Widget] to display before the text. final Widget? prefix; /// Controls the visibility of the [prefix] widget based on the state of /// text entry when the [prefix] argument is not null. /// /// Defaults to [OverlayVisibilityMode.always]. /// /// Has no effect when [prefix] is null. final OverlayVisibilityMode prefixMode; /// An optional [Widget] to display after the text. final Widget? suffix; /// Controls the visibility of the [suffix] widget based on the state of /// text entry when the [suffix] argument is not null. /// /// Defaults to [OverlayVisibilityMode.always]. /// /// Has no effect when [suffix] is null. final OverlayVisibilityMode suffixMode; /// Controls the vertical alignment of the [prefix] and the [suffix] widget in relation to content. /// /// Defaults to [CrossAxisAlignment.center]. /// /// Has no effect when both the [prefix] and [suffix] are null. final CrossAxisAlignment crossAxisAlignment; /// Show an iOS-style clear button to clear the current text entry. /// /// Can be made to appear depending on various text states of the /// [RichTextEditingController]. /// /// Will only appear if no [suffix] widget is appearing. /// /// Defaults to [OverlayVisibilityMode.never]. final OverlayVisibilityMode clearButtonMode; /// The semantic label for the clear button used by screen readers. /// /// This will be used by screen reading software to identify the clear button /// widget. Defaults to "Clear". final String? clearButtonSemanticLabel; /// {@macro flutter.widgets.editableText.keyboardType} final TextInputType keyboardType; /// The type of action button to use for the keyboard. /// /// Defaults to [TextInputAction.newline] if [keyboardType] is /// [TextInputType.multiline] and [TextInputAction.done] otherwise. final TextInputAction? textInputAction; /// {@macro flutter.widgets.editableText.textCapitalization} final TextCapitalization textCapitalization; /// The style to use for the text being edited. /// /// Also serves as a base for the [placeholder] text's style. /// /// Defaults to the standard iOS font style from [CupertinoTheme] if null. final TextStyle? style; /// {@macro flutter.widgets.editableText.strutStyle} final StrutStyle? strutStyle; /// {@macro flutter.widgets.editableText.textAlign} final TextAlign textAlign; /// Configuration of toolbar options. /// /// If not set, select all and paste will default to be enabled. Copy and cut /// will be disabled if [obscureText] is true. If [readOnly] is true, /// paste and cut will be disabled regardless. @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) final ToolbarOptions? toolbarOptions; /// {@macro flutter.material.InputDecorator.textAlignVertical} final TextAlignVertical? textAlignVertical; /// {@macro flutter.widgets.editableText.textDirection} final TextDirection? textDirection; /// {@macro flutter.widgets.editableText.readOnly} final bool readOnly; /// {@macro flutter.widgets.editableText.showCursor} final bool? showCursor; /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; /// {@macro flutter.widgets.editableText.obscuringCharacter} final String obscuringCharacter; /// {@macro flutter.widgets.editableText.obscureText} final bool obscureText; /// {@macro flutter.widgets.editableText.autocorrect} final bool? autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} final SmartQuotesType smartQuotesType; /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} final bool enableSuggestions; /// {@macro flutter.widgets.editableText.maxLines} /// * [expands], which determines whether the field should fill the height of /// its parent. final int? maxLines; /// {@macro flutter.widgets.editableText.minLines} /// * [expands], which determines whether the field should fill the height of /// its parent. final int? minLines; /// {@macro flutter.widgets.editableText.expands} final bool expands; /// The maximum number of characters (Unicode grapheme clusters) to allow in /// the text field. /// /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to /// [MaxLengthEnforcement.none]. /// /// The TextField enforces the length with a /// [LengthLimitingTextInputFormatter], which is evaluated after the supplied /// [inputFormatters], if any. /// /// This value must be either null or greater than zero. If set to null /// (the default), there is no limit to the number of characters allowed. /// /// Whitespace characters (e.g. newline, space, tab) are included in the /// character count. /// /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} final int? maxLength; /// Determines how the [maxLength] limit should be enforced. /// /// If [MaxLengthEnforcement.none] is set, additional input beyond [maxLength] /// will not be enforced by the limit. /// /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} /// /// {@macro flutter.services.textFormatter.maxLengthEnforcement} final MaxLengthEnforcement? maxLengthEnforcement; /// {@macro flutter.widgets.editableText.onChanged} final ValueChanged? onChanged; /// {@macro flutter.widgets.editableText.onEditingComplete} final VoidCallback? onEditingComplete; /// {@macro flutter.widgets.editableText.onSubmitted} /// /// See also: /// /// * [TextInputAction.next] and [TextInputAction.previous], which /// automatically shift the focus to the next/previous focusable item when /// the user is done editing. final ValueChanged? onSubmitted; /// {@macro flutter.widgets.editableText.onTapOutside} final TapRegionCallback? onTapOutside; /// {@macro flutter.widgets.editableText.onTapUpOutside} final TapRegionCallback? onTapUpOutside; /// {@macro flutter.widgets.editableText.inputFormatters} final List? inputFormatters; /// Disables the text field when false. /// /// Text fields in disabled states have a light grey background and don't /// respond to touch events including the [prefix], [suffix] and the clear /// button. /// /// Defaults to true. final bool enabled; /// {@macro flutter.widgets.editableText.cursorWidth} final double cursorWidth; /// {@macro flutter.widgets.editableText.cursorHeight} final double? cursorHeight; /// {@macro flutter.widgets.editableText.cursorRadius} final Radius cursorRadius; /// {@macro flutter.widgets.editableText.cursorOpacityAnimates} final bool cursorOpacityAnimates; /// The color to use when painting the cursor. /// /// Defaults to the [DefaultSelectionStyle.cursorColor]. If that color is /// null, it uses the [CupertinoThemeData.primaryColor] of the ambient theme, /// which itself defaults to [CupertinoColors.activeBlue] in the light theme /// and [CupertinoColors.activeOrange] in the dark theme. final Color? cursorColor; /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. final ui.BoxHeightStyle? selectionHeightStyle; /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. final ui.BoxWidthStyle? selectionWidthStyle; /// The appearance of the keyboard. /// /// This setting is only honored on iOS devices. /// /// If null, defaults to [Brightness.light]. final Brightness? keyboardAppearance; /// {@macro flutter.widgets.editableText.scrollPadding} final EdgeInsets scrollPadding; /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; /// {@macro flutter.widgets.editableText.selectAllOnFocus} final bool? selectAllOnFocus; /// {@macro flutter.widgets.editableText.selectionControls} final TextSelectionControls? selectionControls; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.editableText.scrollController} final ScrollController? scrollController; /// {@macro flutter.widgets.editableText.scrollPhysics} final ScrollPhysics? scrollPhysics; /// {@macro flutter.widgets.editableText.selectionEnabled} bool get selectionEnabled => enableInteractiveSelection; /// {@macro flutter.material.textfield.onTap} final GestureTapCallback? onTap; /// {@macro flutter.widgets.editableText.autofillHints} /// {@macro flutter.services.AutofillConfiguration.autofillHints} final Iterable? autofillHints; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// {@macro flutter.material.textfield.restorationId} final String? restorationId; /// {@macro flutter.widgets.editableText.scribbleEnabled} @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) final bool scribbleEnabled; /// {@macro flutter.widgets.editableText.stylusHandwritingEnabled} final bool stylusHandwritingEnabled; /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} final ContentInsertionConfiguration? contentInsertionConfiguration; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} /// /// If not provided, will build a default menu based on the platform. /// /// See also: /// /// * [CupertinoAdaptiveTextSelectionToolbar], which is built by default. final EditableTextContextMenuBuilder? contextMenuBuilder; static Widget _defaultContextMenuBuilder( BuildContext context, EditableTextState editableTextState, ) { if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText( editableTextState: editableTextState, ); } return CupertinoAdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, ); } /// Configuration for the text field magnifier. /// /// By default (when this property is set to null), a [CupertinoTextMagnifier] /// is used on mobile platforms, and nothing on desktop platforms. To suppress /// the magnifier on all platforms, consider passing /// [TextMagnifierConfiguration.disabled] explicitly. /// /// {@macro flutter.widgets.magnifier.intro} /// /// {@tool dartpad} /// This sample demonstrates how to customize the magnifier that this text field uses. /// /// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart ** /// {@end-tool} final TextMagnifierConfiguration? magnifierConfiguration; /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} /// /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this /// configuration, then [cupertinoMisspelledTextStyle] is used by default. final SpellCheckConfiguration? spellCheckConfiguration; /// The [TextStyle] used to indicate misspelled words in the Cupertino style. /// /// See also: /// * [SpellCheckConfiguration.misspelledTextStyle], the style configured to /// mark misspelled words with. /// * [TextField.materialMisspelledTextStyle], the style configured /// to mark misspelled words with in the Material style. static const TextStyle cupertinoMisspelledTextStyle = TextStyle( decoration: TextDecoration.underline, decorationColor: CupertinoColors.systemRed, decorationStyle: TextDecorationStyle.dotted, ); /// The color of the selection highlight when the spell check menu is visible. /// /// Eyeballed from a screenshot taken on an iPhone 11 running iOS 16.2. @visibleForTesting static const Color kMisspelledSelectionColor = Color(0x62ff9699); /// Default builder for the spell check suggestions toolbar in the Cupertino /// style. /// /// See also: /// * [spellCheckConfiguration], where this is typically specified for /// [CupertinoRichTextField]. /// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the /// parameter for which this is the default value for [CupertinoRichTextField]. /// * [TextField.defaultSpellCheckSuggestionsToolbarBuilder], which is like /// this but specifies the default for [CupertinoRichTextField]. @visibleForTesting static Widget defaultSpellCheckSuggestionsToolbarBuilder( BuildContext context, EditableTextState editableTextState, ) { return CupertinoSpellCheckSuggestionsToolbar.editableText( editableTextState: editableTextState, ); } @override State createState() => _CupertinoRichTextFieldState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( DiagnosticsProperty( 'controller', controller, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'focusNode', focusNode, defaultValue: null, ), ) ..add( DiagnosticsProperty('decoration', decoration), ) ..add(DiagnosticsProperty('padding', padding)) ..add(StringProperty('placeholder', placeholder)) ..add( DiagnosticsProperty('placeholderStyle', placeholderStyle), ) ..add( DiagnosticsProperty( 'prefix', prefix == null ? null : prefixMode, ), ) ..add( DiagnosticsProperty( 'suffix', suffix == null ? null : suffixMode, ), ) ..add( DiagnosticsProperty( 'clearButtonMode', clearButtonMode, ), ) ..add( DiagnosticsProperty( 'clearButtonSemanticLabel', clearButtonSemanticLabel, ), ) ..add( DiagnosticsProperty( 'keyboardType', keyboardType, defaultValue: TextInputType.text, ), ) ..add( DiagnosticsProperty('style', style, defaultValue: null), ) ..add( DiagnosticsProperty('autofocus', autofocus, defaultValue: false), ) ..add( DiagnosticsProperty( 'obscuringCharacter', obscuringCharacter, defaultValue: '•', ), ) ..add( DiagnosticsProperty( 'obscureText', obscureText, defaultValue: false, ), ) ..add( DiagnosticsProperty( 'autocorrect', autocorrect, defaultValue: null, ), ) ..add( EnumProperty( 'smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, ), ) ..add( EnumProperty( 'smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, ), ) ..add( DiagnosticsProperty( 'enableSuggestions', enableSuggestions, defaultValue: true, ), ) ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) ..add(IntProperty('minLines', minLines, defaultValue: null)) ..add( DiagnosticsProperty('expands', expands, defaultValue: false), ) ..add(IntProperty('maxLength', maxLength, defaultValue: null)) ..add( EnumProperty( 'maxLengthEnforcement', maxLengthEnforcement, defaultValue: null, ), ) ..add( DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), ) ..add( DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), ) ..add( DiagnosticsProperty( 'cursorRadius', cursorRadius, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'cursorOpacityAnimates', cursorOpacityAnimates, defaultValue: true, ), ) ..add( createCupertinoColorProperty( 'cursorColor', cursorColor, defaultValue: null, ), ) ..add( FlagProperty( 'selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled', ), ) ..add( DiagnosticsProperty( 'selectionControls', selectionControls, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollController', scrollController, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollPhysics', scrollPhysics, defaultValue: null, ), ) ..add( EnumProperty( 'textAlign', textAlign, defaultValue: TextAlign.start, ), ) ..add( DiagnosticsProperty( 'textAlignVertical', textAlignVertical, defaultValue: null, ), ) ..add( EnumProperty( 'textDirection', textDirection, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'clipBehavior', clipBehavior, defaultValue: Clip.hardEdge, ), ) ..add( DiagnosticsProperty( 'scribbleEnabled', scribbleEnabled, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'stylusHandwritingEnabled', stylusHandwritingEnabled, defaultValue: EditableText.defaultStylusHandwritingEnabled, ), ) ..add( DiagnosticsProperty( 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'spellCheckConfiguration', spellCheckConfiguration, defaultValue: null, ), ) ..add( DiagnosticsProperty>( 'contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const [], defaultValue: contentInsertionConfiguration == null ? const [] : kDefaultContentInsertionMimeTypes, ), ); } static final TextMagnifierConfiguration _iosMagnifierConfiguration = TextMagnifierConfiguration( magnifierBuilder: ( BuildContext context, MagnifierController controller, ValueNotifier magnifierInfo, ) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: return CupertinoTextMagnifier( controller: controller, magnifierInfo: magnifierInfo, ); case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: return null; } }, ); /// Returns a new [SpellCheckConfiguration] where the given configuration has /// had any missing values replaced with their defaults for the iOS platform. static SpellCheckConfiguration inferIOSSpellCheckConfiguration( SpellCheckConfiguration? configuration, ) { if (configuration == null || configuration == const SpellCheckConfiguration.disabled()) { return const SpellCheckConfiguration.disabled(); } return configuration.copyWith( misspelledTextStyle: configuration.misspelledTextStyle ?? CupertinoRichTextField.cupertinoMisspelledTextStyle, misspelledSelectionColor: configuration.misspelledSelectionColor ?? CupertinoRichTextField.kMisspelledSelectionColor, spellCheckSuggestionsToolbarBuilder: configuration.spellCheckSuggestionsToolbarBuilder ?? CupertinoRichTextField.defaultSpellCheckSuggestionsToolbarBuilder, ); } } class _CupertinoRichTextFieldState extends State with RestorationMixin, AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { final GlobalKey _clearGlobalKey = GlobalKey(); RichTextEditingController get _effectiveController => widget.controller; FocusNode? _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement ?? LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(); bool _showSelectionHandles = false; late _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; // API for TextSelectionGestureDetectorBuilderDelegate. @override bool get forcePressEnabled => true; @override final GlobalKey editableTextKey = GlobalKey(); @override bool get selectionEnabled => widget.selectionEnabled; // End of API for TextSelectionGestureDetectorBuilderDelegate. @override void initState() { super.initState(); _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder( state: this, controller: widget.controller, ); _effectiveFocusNode.canRequestFocus = widget.enabled; _effectiveFocusNode.addListener(_handleFocusChanged); } @override void didUpdateWidget(CupertinoRichTextField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); } _effectiveFocusNode.canRequestFocus = widget.enabled; } @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) {} @override String? get restorationId => widget.restorationId; @override void dispose() { _effectiveFocusNode.removeListener(_handleFocusChanged); _focusNode?.dispose(); super.dispose(); } EditableTextState get _editableText => editableTextKey.currentState!; void _requestKeyboard() { _editableText.requestKeyboard(); } void _handleFocusChanged() { setState(() { // Rebuild the widget on focus change to show/hide the text selection // highlight. }); } bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { // When the text field is activated by something that doesn't trigger the // selection toolbar, we shouldn't show the handles either. if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar || !_selectionGestureDetectorBuilder.shouldShowSelectionHandles) { return false; } // On iOS, we don't show handles when the selection is collapsed. if (_effectiveController.selection.isCollapsed) { return false; } if (cause == SelectionChangedCause.keyboard) { return false; } if (cause == SelectionChangedCause.stylusHandwriting) { return true; } if (_effectiveController.text.isNotEmpty) { return true; } return false; } void _handleSelectionChanged( TextSelection selection, SelectionChangedCause? cause, ) { final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); if (willShowSelectionHandles != _showSelectionHandles) { setState(() { _showSelectionHandles = willShowSelectionHandles; }); } switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.fuchsia: case TargetPlatform.android: if (cause == SelectionChangedCause.longPress) { _editableText.bringIntoView(selection.extent); } } switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.fuchsia: case TargetPlatform.android: break; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: if (cause == SelectionChangedCause.drag) { _editableText.hideToolbar(); } } } @override bool get wantKeepAlive => _effectiveController.value.text.isNotEmpty; static bool _shouldShowAttachment({ required OverlayVisibilityMode attachment, required bool hasText, }) { return switch (attachment) { OverlayVisibilityMode.never => false, OverlayVisibilityMode.always => true, OverlayVisibilityMode.editing => hasText, OverlayVisibilityMode.notEditing => !hasText, }; } // True if any surrounding decoration widgets will be shown. bool get _hasDecoration { return widget.placeholder != null || widget.clearButtonMode != OverlayVisibilityMode.never || widget.prefix != null || widget.suffix != null; } // Provide default behavior if widget.textAlignVertical is not set. // CupertinoTextField has top alignment by default, unless it has decoration // like a prefix or suffix, in which case it's aligned to the center. TextAlignVertical get _textAlignVertical { if (widget.textAlignVertical != null) { return widget.textAlignVertical!; } return _hasDecoration ? TextAlignVertical.center : TextAlignVertical.top; } void _onClearButtonTapped() { final bool hadText = _effectiveController.text.isNotEmpty; _effectiveController.clear(); if (hadText) { // Tapping the clear button is also considered a "user initiated" change // (instead of a programmatical one), so call `onChanged` if the text // changed as a result. widget.onChanged?.call(_effectiveController.text); } } Widget _buildClearButton() { final String clearLabel = widget.clearButtonSemanticLabel ?? CupertinoLocalizations.of(context).clearButtonLabel; return Semantics( button: true, label: clearLabel, child: GestureDetector( key: _clearGlobalKey, onTap: widget.enabled ? _onClearButtonTapped : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Icon( CupertinoIcons.clear_thick_circled, size: 18.0, color: CupertinoDynamicColor.resolve(_kClearButtonColor, context), ), ), ), ); } Widget _addTextDependentAttachments( Widget editableText, TextStyle textStyle, TextStyle placeholderStyle, ) { // If there are no surrounding widgets, just return the core editable text // part. if (!_hasDecoration) { return editableText; } // Otherwise, listen to the current state of the text entry. return ValueListenableBuilder( valueListenable: _effectiveController, child: editableText, builder: (BuildContext context, TextEditingValue text, Widget? child) { final bool hasText = text.text.isNotEmpty; final String? placeholderText = widget.placeholder; final Widget? placeholder = placeholderText == null ? null // Make the placeholder invisible when hasText is true. : Visibility( maintainAnimation: true, maintainSize: true, maintainState: true, visible: !hasText, child: SizedBox( width: double.infinity, child: Padding( padding: widget.padding, child: Text( placeholderText, // This is to make sure the text field is always tall enough // to accommodate the first line of the placeholder, so the // text does not shrink vertically as you type (however in // rare circumstances, the height may still change when // there's no placeholder text). maxLines: hasText ? 1 : widget.maxLines, overflow: placeholderStyle.overflow, style: placeholderStyle, textAlign: widget.textAlign, ), ), ), ); final Widget? prefixWidget = _shouldShowAttachment( attachment: widget.prefixMode, hasText: hasText, ) ? widget.prefix : null; // Show user specified suffix if applicable and fall back to clear button. final bool showUserSuffix = _shouldShowAttachment( attachment: widget.suffixMode, hasText: hasText, ); final bool showClearButton = _shouldShowAttachment( attachment: widget.clearButtonMode, hasText: hasText, ); final Widget? suffixWidget = switch (( showUserSuffix, showClearButton, )) { (false, false) => null, (true, false) => widget.suffix, (true, true) => widget.suffix ?? _buildClearButton(), (false, true) => _buildClearButton(), }; return Row( crossAxisAlignment: widget.crossAxisAlignment, children: [ // Insert a prefix at the front if the prefix visibility mode matches // the current text state. ?prefixWidget, // In the middle part, stack the placeholder on top of the main EditableText // if needed. Expanded( child: Directionality( textDirection: widget.textDirection ?? Directionality.of(context), child: _BaselineAlignedStack( placeholder: placeholder, editableText: editableText, textAlignVertical: _textAlignVertical, editableTextBaseline: textStyle.textBaseline ?? TextBaseline.alphabetic, placeholderBaseline: placeholderStyle.textBaseline ?? TextBaseline.alphabetic, ), ), ), ?suffixWidget, ], ); }, ); } // AutofillClient implementation start. @override String get autofillId => _editableText.autofillId; @override void autofill(TextEditingValue newEditingValue) => _editableText.autofill(newEditingValue); @override TextInputConfiguration get textInputConfiguration { final List? autofillHints = widget.autofillHints?.toList( growable: false, ); final AutofillConfiguration autofillConfiguration = autofillHints != null ? AutofillConfiguration( uniqueIdentifier: autofillId, autofillHints: autofillHints, currentEditingValue: _effectiveController.value, hintText: widget.placeholder, ) : AutofillConfiguration.disabled; return _editableText.textInputConfiguration.copyWith( autofillConfiguration: autofillConfiguration, ); } // AutofillClient implementation end. @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. assert(debugCheckHasDirectionality(context)); final RichTextEditingController controller = _effectiveController; TextSelectionControls? textSelectionControls = widget.selectionControls; VoidCallback? handleDidGainAccessibilityFocus; VoidCallback? handleDidLoseAccessibilityFocus; switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.android: case TargetPlatform.fuchsia: textSelectionControls ??= cupertinoTextSelectionHandleControls; case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls; handleDidGainAccessibilityFocus = () { // Automatically activate the TextField when it receives accessibility focus. if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { _effectiveFocusNode.requestFocus(); } }; handleDidLoseAccessibilityFocus = () { _effectiveFocusNode.unfocus(); }; } final bool enabled = widget.enabled; final cursorOffset = Offset( _iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0, ); final formatters = [ ...?widget.inputFormatters, if (widget.maxLength != null) LengthLimitingTextInputFormatter( widget.maxLength, maxLengthEnforcement: _effectiveMaxLengthEnforcement, ), ]; final CupertinoThemeData themeData = CupertinoTheme.of(context); final TextStyle? resolvedStyle = widget.style?.copyWith( color: CupertinoDynamicColor.maybeResolve(widget.style?.color, context), backgroundColor: CupertinoDynamicColor.maybeResolve( widget.style?.backgroundColor, context, ), ); final TextStyle textStyle = themeData.textTheme.textStyle.merge( resolvedStyle, ); final TextStyle? resolvedPlaceholderStyle = widget.placeholderStyle ?.copyWith( color: CupertinoDynamicColor.maybeResolve( widget.placeholderStyle?.color, context, ), backgroundColor: CupertinoDynamicColor.maybeResolve( widget.placeholderStyle?.backgroundColor, context, ), ); final TextStyle placeholderStyle = textStyle.merge( resolvedPlaceholderStyle, ); final Brightness keyboardAppearance = widget.keyboardAppearance ?? CupertinoTheme.brightnessOf(context); final Color cursorColor = CupertinoDynamicColor.maybeResolve( widget.cursorColor ?? DefaultSelectionStyle.of(context).cursorColor, context, ) ?? themeData.primaryColor; final Color disabledColor = CupertinoDynamicColor.resolve( _kDisabledBackground, context, ); final Color? decorationColor = CupertinoDynamicColor.maybeResolve( widget.decoration?.color, context, ); final BoxBorder? border = widget.decoration?.border; var resolvedBorder = border as Border?; if (border is Border) { BorderSide resolveBorderSide(BorderSide side) { return side == BorderSide.none ? side : side.copyWith( color: CupertinoDynamicColor.resolve(side.color, context), ); } resolvedBorder = border.runtimeType != Border ? border : Border( top: resolveBorderSide(border.top), left: resolveBorderSide(border.left), bottom: resolveBorderSide(border.bottom), right: resolveBorderSide(border.right), ); } // Use the default disabled color only if the box decoration was not set. final BoxDecoration? effectiveDecoration = widget.decoration?.copyWith( border: resolvedBorder, color: enabled ? decorationColor : (widget.decoration == _kDefaultRoundedBorderDecoration ? disabledColor : widget.decoration?.color), ); final Color selectionColor = CupertinoDynamicColor.maybeResolve( DefaultSelectionStyle.of(context).selectionColor, context, ) ?? CupertinoTheme.of(context).primaryColor.withValues(alpha: 0.2); // Set configuration as disabled if not otherwise specified. If specified, // ensure that configuration uses Cupertino text style for misspelled words // unless a custom style is specified. final SpellCheckConfiguration spellCheckConfiguration = CupertinoRichTextField.inferIOSSpellCheckConfiguration( widget.spellCheckConfiguration, ); final Widget paddedEditable = Padding( padding: widget.padding, child: RepaintBoundary( child: UnmanagedRestorationScope( bucket: bucket, child: EditableText( key: editableTextKey, controller: controller, readOnly: widget.readOnly || !enabled, toolbarOptions: widget.toolbarOptions, showCursor: widget.showCursor, showSelectionHandles: _showSelectionHandles, focusNode: _effectiveFocusNode, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, textCapitalization: widget.textCapitalization, style: textStyle, strutStyle: widget.strutStyle, textAlign: widget.textAlign, textDirection: widget.textDirection, autofocus: widget.autofocus, obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, autocorrect: widget.autocorrect, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, enableSuggestions: widget.enableSuggestions, maxLines: widget.maxLines, minLines: widget.minLines, expands: widget.expands, magnifierConfiguration: widget.magnifierConfiguration ?? CupertinoRichTextField._iosMagnifierConfiguration, // Only show the selection highlight when the text field is focused. selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null, selectionControls: widget.selectionEnabled ? textSelectionControls : null, groupId: widget.groupId, onChanged: widget.onChanged, onSelectionChanged: _handleSelectionChanged, onEditingComplete: widget.onEditingComplete, onSubmitted: widget.onSubmitted, onTapOutside: widget.onTapOutside, inputFormatters: formatters, rendererIgnoresPointer: true, cursorWidth: widget.cursorWidth, cursorHeight: widget.cursorHeight, cursorRadius: widget.cursorRadius, cursorColor: cursorColor, cursorOpacityAnimates: widget.cursorOpacityAnimates, cursorOffset: cursorOffset, paintCursorAboveText: true, autocorrectionTextRectColor: selectionColor, backgroundCursorColor: CupertinoDynamicColor.resolve( CupertinoColors.inactiveGray, context, ), selectionHeightStyle: widget.selectionHeightStyle, selectionWidthStyle: widget.selectionWidthStyle, scrollPadding: widget.scrollPadding, keyboardAppearance: keyboardAppearance, dragStartBehavior: widget.dragStartBehavior, scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, enableInteractiveSelection: widget.enableInteractiveSelection, selectAllOnFocus: widget.selectAllOnFocus, autofillClient: this, clipBehavior: widget.clipBehavior, restorationId: 'editable', scribbleEnabled: widget.scribbleEnabled, stylusHandwritingEnabled: widget.stylusHandwritingEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, contentInsertionConfiguration: widget.contentInsertionConfiguration, contextMenuBuilder: widget.contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, ), ), ), ); return Semantics( enabled: enabled, onTap: !enabled || widget.readOnly ? null : () { if (!controller.selection.isValid) { controller.selection = TextSelection.collapsed( offset: controller.text.length, ); } _requestKeyboard(); }, onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus, onFocus: enabled ? () { assert( _effectiveFocusNode.canRequestFocus, 'Received SemanticsAction.focus from the engine. However, the FocusNode ' 'of this text field cannot gain focus. This likely indicates a bug. ' 'If this text field cannot be focused (e.g. because it is not ' 'enabled), then its corresponding semantics node must be configured ' 'such that the assistive technology cannot request focus on it.', ); if (_effectiveFocusNode.canRequestFocus && !_effectiveFocusNode.hasFocus) { _effectiveFocusNode.requestFocus(); } else if (!widget.readOnly) { // If the platform requested focus, that means that previously the // platform believed that the text field did not have focus (even // though Flutter's widget system believed otherwise). This likely // means that the on-screen keyboard is hidden, or more generally, // there is no current editing session in this field. To correct // that, keyboard must be requested. // // A concrete scenario where this can happen is when the user // dismisses the keyboard on the web. The editing session is // closed by the engine, but the text field widget stays focused // in the framework. _requestKeyboard(); } } : null, child: TextFieldTapRegion( child: IgnorePointer( ignoring: !enabled, child: Container( decoration: effectiveDecoration, color: !enabled && effectiveDecoration == null ? disabledColor : null, child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: Align( alignment: Alignment(-1.0, _textAlignVertical.y), widthFactor: 1.0, heightFactor: 1.0, child: _addTextDependentAttachments( paddedEditable, textStyle, placeholderStyle, ), ), ), ), ), ), ); } } enum _BaselineAlignedStackSlot { placeholder, editableText } class _BaselineAlignedStack extends SlottedMultiChildRenderObjectWidget< _BaselineAlignedStackSlot, RenderBox > { const _BaselineAlignedStack({ required this.editableTextBaseline, required this.placeholderBaseline, required this.textAlignVertical, required this.editableText, this.placeholder, }); final TextBaseline editableTextBaseline; final TextBaseline placeholderBaseline; final TextAlignVertical textAlignVertical; final Widget editableText; final Widget? placeholder; @override Iterable<_BaselineAlignedStackSlot> get slots => _BaselineAlignedStackSlot.values; @override Widget? childForSlot(_BaselineAlignedStackSlot slot) { return switch (slot) { _BaselineAlignedStackSlot.placeholder => placeholder, _BaselineAlignedStackSlot.editableText => editableText, }; } @override _RenderBaselineAlignedStack createRenderObject(BuildContext context) { return _RenderBaselineAlignedStack( textAlignVertical: textAlignVertical, editableTextBaseline: editableTextBaseline, placeholderBaseline: placeholderBaseline, ); } @override void updateRenderObject( BuildContext context, _RenderBaselineAlignedStack renderObject, ) { renderObject ..textAlignVertical = textAlignVertical ..editableTextBaseline = editableTextBaseline ..placeholderBaseline = placeholderBaseline; } } class _BaselineAlignedStackParentData extends ContainerBoxParentData {} class _RenderBaselineAlignedStack extends RenderBox with SlottedContainerRenderObjectMixin< _BaselineAlignedStackSlot, RenderBox > { _RenderBaselineAlignedStack({ required TextAlignVertical textAlignVertical, required TextBaseline editableTextBaseline, required TextBaseline placeholderBaseline, }) : _textAlignVertical = textAlignVertical, _editableTextBaseline = editableTextBaseline, _placeholderBaseline = placeholderBaseline; TextAlignVertical get textAlignVertical => _textAlignVertical; TextAlignVertical _textAlignVertical; set textAlignVertical(TextAlignVertical value) { if (_textAlignVertical == value) { return; } _textAlignVertical = value; markNeedsLayout(); } TextBaseline get editableTextBaseline => _editableTextBaseline; TextBaseline _editableTextBaseline; set editableTextBaseline(TextBaseline value) { if (_editableTextBaseline == value) { return; } _editableTextBaseline = value; markNeedsLayout(); } TextBaseline get placeholderBaseline => _placeholderBaseline; TextBaseline _placeholderBaseline; set placeholderBaseline(TextBaseline value) { if (_placeholderBaseline == value) { return; } _placeholderBaseline = value; markNeedsLayout(); } @override void setupParentData(RenderBox child) { if (child.parentData is! _BaselineAlignedStackParentData) { child.parentData = _BaselineAlignedStackParentData(); } } RenderBox? get _placeholderChild { return childForSlot(_BaselineAlignedStackSlot.placeholder); } RenderBox get _editableTextChild { final RenderBox? child = childForSlot( _BaselineAlignedStackSlot.editableText, ); assert(child != null); return child!; } @override double computeMinIntrinsicHeight(double width) { return math.max( _placeholderChild?.getMinIntrinsicHeight(width) ?? 0.0, _editableTextChild.getMinIntrinsicHeight(width), ); } @override double computeMaxIntrinsicHeight(double width) { return math.max( _placeholderChild?.getMaxIntrinsicHeight(width) ?? 0.0, _editableTextChild.getMaxIntrinsicHeight(width), ); } @override double computeMinIntrinsicWidth(double height) { return math.max( _placeholderChild?.getMinIntrinsicWidth(height) ?? 0.0, _editableTextChild.getMinIntrinsicWidth(height), ); } @override double computeMaxIntrinsicWidth(double height) { return math.max( _placeholderChild?.getMaxIntrinsicWidth(height) ?? 0.0, _editableTextChild.getMaxIntrinsicWidth(height), ); } @override void performLayout() { assert(constraints.hasTightWidth); final RenderBox? placeholder = _placeholderChild; final RenderBox editableText = _editableTextChild; final editableTextParentData = editableText.parentData! as _BaselineAlignedStackParentData; final placeholderParentData = placeholder?.parentData as _BaselineAlignedStackParentData?; size = _computeSize( constraints: constraints, layoutChild: ChildLayoutHelper.layoutChild, getBaseline: ChildLayoutHelper.getBaseline, ); final double editableTextBaselineValue = editableText.getDistanceToBaseline( editableTextBaseline, )!; final double? placeholderBaselineValue = placeholder?.getDistanceToBaseline( placeholderBaseline, ); assert(placeholder != null || placeholderBaselineValue == null); final Offset baselineDiff = placeholderBaselineValue != null ? Offset(0.0, editableTextBaselineValue - placeholderBaselineValue) : Offset.zero; final verticalAlignment = Alignment(0.0, textAlignVertical.y); editableTextParentData.offset = verticalAlignment.alongOffset( size - editableText.size as Offset, ); // Baseline-align the placeholder to the editable text. placeholderParentData?.offset = editableTextParentData.offset + baselineDiff; } @override void paint(PaintingContext context, Offset offset) { final RenderBox? placeholder = _placeholderChild; final RenderBox editableText = _editableTextChild; if (placeholder != null) { final placeholderParentData = placeholder.parentData! as _BaselineAlignedStackParentData; context.paintChild(placeholder, offset + placeholderParentData.offset); } final editableTextParentData = editableText.parentData! as _BaselineAlignedStackParentData; context.paintChild(editableText, offset + editableTextParentData.offset); } @override Size computeDryLayout(covariant BoxConstraints constraints) { return _computeSize( constraints: constraints, layoutChild: ChildLayoutHelper.dryLayoutChild, getBaseline: ChildLayoutHelper.getDryBaseline, ); } Size _computeSize({ required BoxConstraints constraints, required ChildLayouter layoutChild, required ChildBaselineGetter getBaseline, }) { double width = constraints.minWidth; double height = constraints.minHeight; final RenderBox editableText = _editableTextChild; final Size editableTextSize = layoutChild(editableText, constraints); final double editableTextBaselineValue = getBaseline( editableText, constraints, editableTextBaseline, )!; final double editableTextDescent = editableTextSize.height - editableTextBaselineValue; Size? placeholderSize; double? placeholderBaselineValue; final RenderBox? placeholder = _placeholderChild; if (placeholder != null) { placeholderSize = layoutChild(placeholder, constraints); width = math.max(width, placeholderSize.width); placeholderBaselineValue = getBaseline( placeholder, constraints, placeholderBaseline, ); final double placeholderDescent = placeholderSize.height - placeholderBaselineValue!; // The size is the sum of the placeholder's max ascent and descent and the // editable text's max ascent and descent. final double maxExtentBaseline = math.max(editableTextBaselineValue, placeholderBaselineValue) + math.max(editableTextDescent, placeholderDescent); height = math.max(height, maxExtentBaseline); } height = math.max(height, editableTextSize.height); width = math.max(width, editableTextSize.width); final size = Size(width, height); assert(size.isFinite); return constraints.constrain(size); } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { final RenderBox editableText = _editableTextChild; final editableTextParentData = editableText.parentData! as _BaselineAlignedStackParentData; return result.addWithPaintOffset( offset: editableTextParentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset transformed) { assert(transformed == position - editableTextParentData.offset); return editableText.hitTest(result, position: transformed); }, ); } } ================================================ FILE: lib/common/widgets/flutter/text_field/editable.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/cupertino.dart'; library; import 'dart:math' as math; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, LineMetrics, SemanticsInputType, TextBox; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:characters/characters.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; const double _kCaretGap = 1.0; // pixels const double _kCaretHeightOffset = 2.0; // pixels // The additional size on the x and y axis with which to expand the prototype // cursor to render the floating cursor in pixels. const EdgeInsets _kFloatingCursorSizeIncrease = EdgeInsets.symmetric( horizontal: 0.5, vertical: 1.0, ); // The corner radius of the floating cursor in pixels. const Radius _kFloatingCursorRadius = Radius.circular(1.0); // This constant represents the shortest squared distance required between the floating cursor // and the regular cursor when both are present in the text field. // If the squared distance between the two cursors is less than this value, // it's not necessary to display both cursors at the same time. // This behavior is consistent with the one observed in iOS UITextField. const double _kShortestDistanceSquaredWithFloatingAndRegularCursors = 15.0 * 15.0; /// The consecutive sequence of [TextPosition]s that the caret should move to /// when the user navigates the paragraph using the upward arrow key or the /// downward arrow key. /// /// {@template flutter.rendering.RenderEditable.verticalArrowKeyMovement} /// When the user presses the upward arrow key or the downward arrow key, on /// many platforms (macOS for instance), the caret will move to the previous /// line or the next line, while maintaining its original horizontal location. /// When it encounters a shorter line, the caret moves to the closest horizontal /// location within that line, and restores the original horizontal location /// when a long enough line is encountered. /// /// Additionally, the caret will move to the beginning of the document if the /// upward arrow key is pressed and the caret is already on the first line. If /// the downward arrow key is pressed next, the caret will restore its original /// horizontal location and move to the second line. Similarly the caret moves /// to the end of the document if the downward arrow key is pressed when it's /// already on the last line. /// /// Consider a left-aligned paragraph: /// aa| /// a /// aaa /// where the caret was initially placed at the end of the first line. Pressing /// the downward arrow key once will move the caret to the end of the second /// line, and twice the arrow key moves to the third line after the second "a" /// on that line. Pressing the downward arrow key again, the caret will move to /// the end of the third line (the end of the document). Pressing the upward /// arrow key in this state will result in the caret moving to the end of the /// second line. /// /// Vertical caret runs are typically interrupted when the layout of the text /// changes (including when the text itself changes), or when the selection is /// changed by other input events or programmatically (for example, when the /// user pressed the left arrow key). /// {@endtemplate} /// /// The [movePrevious] method moves the caret location (which is /// [VerticalCaretMovementRun.current]) to the previous line, and in case /// the caret is already on the first line, the method does nothing and returns /// false. Similarly the [moveNext] method moves the caret to the next line, and /// returns false if the caret is already on the last line. /// /// The [moveByOffset] method takes a pixel offset from the current position to move /// the caret up or down. /// /// If the underlying paragraph's layout changes, [isValid] becomes false and /// the [VerticalCaretMovementRun] must not be used. The [isValid] property must /// be checked before calling [movePrevious], [moveNext] and [moveByOffset], /// or accessing [current]. class VerticalCaretMovementRun implements Iterator { VerticalCaretMovementRun._( this._editable, this._lineMetrics, this._currentTextPosition, this._currentLine, this._currentOffset, ); Offset _currentOffset; int _currentLine; TextPosition _currentTextPosition; final List _lineMetrics; final RenderEditable _editable; bool _isValid = true; /// Whether this [VerticalCaretMovementRun] can still continue. /// /// A [VerticalCaretMovementRun] run is valid if the underlying text layout /// hasn't changed. /// /// The [current] value and the [movePrevious], [moveNext] and [moveByOffset] /// methods must not be accessed when [isValid] is false. bool get isValid { if (!_isValid) { return false; } final List newLineMetrics = _editable._textPainter .computeLineMetrics(); // Use the implementation detail of the computeLineMetrics method to figure // out if the current text layout has been invalidated. if (!identical(newLineMetrics, _lineMetrics)) { _isValid = false; } return _isValid; } final Map> _positionCache = >{}; MapEntry _getTextPositionForLine(int lineNumber) { assert(isValid); assert(lineNumber >= 0); final MapEntry? cachedPosition = _positionCache[lineNumber]; if (cachedPosition != null) { return cachedPosition; } assert(lineNumber != _currentLine); final newOffset = Offset( _currentOffset.dx, _lineMetrics[lineNumber].baseline, ); final TextPosition closestPosition = _editable._textPainter .getPositionForOffset(newOffset); final position = MapEntry(newOffset, closestPosition); _positionCache[lineNumber] = position; return position; } @override TextPosition get current { assert(isValid); return _currentTextPosition; } @override bool moveNext() { assert(isValid); if (_currentLine + 1 >= _lineMetrics.length) { return false; } final MapEntry position = _getTextPositionForLine( _currentLine + 1, ); _currentLine += 1; _currentOffset = position.key; _currentTextPosition = position.value; return true; } /// Move back to the previous element. /// /// Returns true and updates [current] if successful. bool movePrevious() { assert(isValid); if (_currentLine <= 0) { return false; } final MapEntry position = _getTextPositionForLine( _currentLine - 1, ); _currentLine -= 1; _currentOffset = position.key; _currentTextPosition = position.value; return true; } /// Move forward or backward by a number of elements determined /// by pixel [offset]. /// /// If [offset] is negative, move backward; otherwise move forward. /// /// Returns true and updates [current] if successful. bool moveByOffset(double offset) { final Offset initialOffset = _currentOffset; if (offset >= 0.0) { while (_currentOffset.dy < initialOffset.dy + offset) { if (!moveNext()) { break; } } } else { while (_currentOffset.dy > initialOffset.dy + offset) { if (!movePrevious()) { break; } } } return initialOffset != _currentOffset; } } /// Displays some text in a scrollable container with a potentially blinking /// cursor and with gesture recognizers. /// /// This is the renderer for an editable text field. It does not directly /// provide affordances for editing the text, but it does handle text selection /// and manipulation of the text cursor. /// /// The [text] is displayed, scrolled by the given [offset], aligned according /// to [textAlign]. The [maxLines] property controls whether the text displays /// on one line or many. The [selection], if it is not collapsed, is painted in /// the [selectionColor]. If it _is_ collapsed, then it represents the cursor /// position. The cursor is shown while [showCursor] is true. It is painted in /// the [cursorColor]. /// /// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value /// to actually blink the cursor, and other features not mentioned above are the /// responsibility of higher layers and not handled by this object. class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin, RenderInlineChildrenContainerDefaults implements TextLayoutMetrics { /// Creates a render object that implements the visual aspects of a text field. /// /// The [textAlign] argument defaults to [TextAlign.start]. /// /// If [showCursor] is not specified, then it defaults to hiding the cursor. /// /// The [maxLines] property can be set to null to remove the restriction on /// the number of lines. By default, it is 1, meaning this is a single-line /// text field. If it is not null, it must be greater than zero. /// /// Use [ViewportOffset.zero] for the [offset] if there is no need for /// scrolling. RenderEditable({ InlineSpan? text, required TextDirection textDirection, TextAlign textAlign = TextAlign.start, Color? cursorColor, Color? backgroundCursorColor, ValueNotifier? showCursor, bool? hasFocus, required LayerLink startHandleLayerLink, required LayerLink endHandleLayerLink, int? maxLines = 1, int? minLines, bool expands = false, StrutStyle? strutStyle, Color? selectionColor, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) double textScaleFactor = 1.0, TextScaler textScaler = TextScaler.noScaling, TextSelection? selection, required ViewportOffset offset, this.ignorePointer = false, bool readOnly = false, bool forceLine = true, TextHeightBehavior? textHeightBehavior, TextWidthBasis textWidthBasis = TextWidthBasis.parent, String obscuringCharacter = '•', bool obscureText = false, Locale? locale, double cursorWidth = 1.0, double? cursorHeight, Radius? cursorRadius, bool paintCursorAboveText = false, Offset cursorOffset = Offset.zero, double devicePixelRatio = 1.0, ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.max, ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.max, bool? enableInteractiveSelection, this.floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5), TextRange? promptRectRange, Color? promptRectColor, Clip clipBehavior = Clip.hardEdge, required this.textSelectionDelegate, RenderEditablePainter? painter, RenderEditablePainter? foregroundPainter, List? children, required this.controller, }) : assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( !expands || (maxLines == null && minLines == null), 'minLines and maxLines must be null when expands is true.', ), assert( identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0, 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', ), assert(obscuringCharacter.characters.length == 1), assert(cursorWidth >= 0.0), assert(cursorHeight == null || cursorHeight >= 0.0), _textPainter = TextPainter( text: text, textAlign: textAlign, textDirection: textDirection, textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, locale: locale, maxLines: maxLines == 1 ? 1 : null, strutStyle: strutStyle, textHeightBehavior: textHeightBehavior, textWidthBasis: textWidthBasis, ), _showCursor = showCursor ?? ValueNotifier(false), _maxLines = maxLines, _minLines = minLines, _expands = expands, _selection = selection, _offset = offset, _cursorWidth = cursorWidth, _cursorHeight = cursorHeight, _paintCursorOnTop = paintCursorAboveText, _enableInteractiveSelection = enableInteractiveSelection, _devicePixelRatio = devicePixelRatio, _startHandleLayerLink = startHandleLayerLink, _endHandleLayerLink = endHandleLayerLink, _obscuringCharacter = obscuringCharacter, _obscureText = obscureText, _readOnly = readOnly, _forceLine = forceLine, _clipBehavior = clipBehavior, _hasFocus = hasFocus ?? false, _disposeShowCursor = showCursor == null { assert(!_showCursor.value || cursorColor != null); _selectionPainter.highlightColor = selectionColor; _selectionPainter.highlightedRange = selection; _selectionPainter.selectionHeightStyle = selectionHeightStyle; _selectionPainter.selectionWidthStyle = selectionWidthStyle; _autocorrectHighlightPainter.highlightColor = promptRectColor; _autocorrectHighlightPainter.highlightedRange = promptRectRange; _caretPainter.caretColor = cursorColor; _caretPainter.cursorRadius = cursorRadius; _caretPainter.cursorOffset = cursorOffset; _caretPainter.backgroundCursorColor = backgroundCursorColor; _updateForegroundPainter(foregroundPainter); _updatePainter(painter); addAll(children); } final RichTextEditingController controller; /// Child render objects _RenderEditableCustomPaint? _foregroundRenderObject; _RenderEditableCustomPaint? _backgroundRenderObject; @override void dispose() { _leaderLayerHandler.layer = null; _foregroundRenderObject?.dispose(); _foregroundRenderObject = null; _backgroundRenderObject?.dispose(); _backgroundRenderObject = null; _clipRectLayer.layer = null; _cachedBuiltInForegroundPainters?.dispose(); _cachedBuiltInPainters?.dispose(); _selectionStartInViewport.dispose(); _selectionEndInViewport.dispose(); _autocorrectHighlightPainter.dispose(); _selectionPainter.dispose(); _caretPainter.dispose(); _textPainter.dispose(); _textIntrinsicsCache?.dispose(); if (_disposeShowCursor) { _showCursor.dispose(); _disposeShowCursor = false; } super.dispose(); } void _updateForegroundPainter(RenderEditablePainter? newPainter) { final _CompositeRenderEditablePainter effectivePainter = newPainter == null ? _builtInForegroundPainters : _CompositeRenderEditablePainter( painters: [ _builtInForegroundPainters, newPainter, ], ); if (_foregroundRenderObject == null) { final foregroundRenderObject = _RenderEditableCustomPaint( painter: effectivePainter, ); adoptChild(foregroundRenderObject); _foregroundRenderObject = foregroundRenderObject; } else { _foregroundRenderObject?.painter = effectivePainter; } _foregroundPainter = newPainter; } /// The [RenderEditablePainter] to use for painting above this /// [RenderEditable]'s text content. /// /// The new [RenderEditablePainter] will replace the previously specified /// foreground painter, and schedule a repaint if the new painter's /// `shouldRepaint` method returns true. RenderEditablePainter? get foregroundPainter => _foregroundPainter; RenderEditablePainter? _foregroundPainter; set foregroundPainter(RenderEditablePainter? newPainter) { if (newPainter == _foregroundPainter) { return; } _updateForegroundPainter(newPainter); } void _updatePainter(RenderEditablePainter? newPainter) { final _CompositeRenderEditablePainter effectivePainter = newPainter == null ? _builtInPainters : _CompositeRenderEditablePainter( painters: [_builtInPainters, newPainter], ); if (_backgroundRenderObject == null) { final backgroundRenderObject = _RenderEditableCustomPaint( painter: effectivePainter, ); adoptChild(backgroundRenderObject); _backgroundRenderObject = backgroundRenderObject; } else { _backgroundRenderObject?.painter = effectivePainter; } _painter = newPainter; } /// Sets the [RenderEditablePainter] to use for painting beneath this /// [RenderEditable]'s text content. /// /// The new [RenderEditablePainter] will replace the previously specified /// painter, and schedule a repaint if the new painter's `shouldRepaint` /// method returns true. RenderEditablePainter? get painter => _painter; RenderEditablePainter? _painter; set painter(RenderEditablePainter? newPainter) { if (newPainter == _painter) { return; } _updatePainter(newPainter); } // Caret Painters: // A single painter for both the regular caret and the floating cursor. late final _CaretPainter _caretPainter = _CaretPainter(); // Text Highlight painters: final _TextHighlightPainter _selectionPainter = _TextHighlightPainter(); final _TextHighlightPainter _autocorrectHighlightPainter = _TextHighlightPainter(); _CompositeRenderEditablePainter get _builtInForegroundPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters(); _CompositeRenderEditablePainter? _cachedBuiltInForegroundPainters; _CompositeRenderEditablePainter _createBuiltInForegroundPainters() { return _CompositeRenderEditablePainter( painters: [ if (paintCursorAboveText) _caretPainter, ], ); } _CompositeRenderEditablePainter get _builtInPainters => _cachedBuiltInPainters ??= _createBuiltInPainters(); _CompositeRenderEditablePainter? _cachedBuiltInPainters; _CompositeRenderEditablePainter _createBuiltInPainters() { return _CompositeRenderEditablePainter( painters: [ _autocorrectHighlightPainter, _selectionPainter, if (!paintCursorAboveText) _caretPainter, ], ); } /// Whether the [handleEvent] will propagate pointer events to selection /// handlers. /// /// If this property is true, the [handleEvent] assumes that this renderer /// will be notified of input gestures via [handleTapDown], [handleTap], /// [handleDoubleTap], and [handleLongPress]. /// /// If there are any gesture recognizers in the text span, the [handleEvent] /// will still propagate pointer events to those recognizers. /// /// The default value of this property is false. bool ignorePointer; /// {@macro dart.ui.textHeightBehavior} TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior; set textHeightBehavior(TextHeightBehavior? value) { if (_textPainter.textHeightBehavior == value) { return; } _textPainter.textHeightBehavior = value; markNeedsLayout(); } /// {@macro flutter.painting.textPainter.textWidthBasis} TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis; set textWidthBasis(TextWidthBasis value) { if (_textPainter.textWidthBasis == value) { return; } _textPainter.textWidthBasis = value; markNeedsLayout(); } /// The pixel ratio of the current device. /// /// Should be obtained by querying MediaQuery for the devicePixelRatio. double get devicePixelRatio => _devicePixelRatio; double _devicePixelRatio; set devicePixelRatio(double value) { if (devicePixelRatio == value) { return; } _devicePixelRatio = value; markNeedsLayout(); } /// Character used for obscuring text if [obscureText] is true. /// /// Must have a length of exactly one. String get obscuringCharacter => _obscuringCharacter; String _obscuringCharacter; set obscuringCharacter(String value) { if (_obscuringCharacter == value) { return; } assert(value.characters.length == 1); _obscuringCharacter = value; markNeedsLayout(); } /// Whether to hide the text being edited (e.g., for passwords). bool get obscureText => _obscureText; bool _obscureText; set obscureText(bool value) { if (_obscureText == value) { return; } _obscureText = value; _cachedAttributedValue = null; markNeedsSemanticsUpdate(); } /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. ui.BoxHeightStyle get selectionHeightStyle => _selectionPainter.selectionHeightStyle; set selectionHeightStyle(ui.BoxHeightStyle value) { _selectionPainter.selectionHeightStyle = value; } /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. ui.BoxWidthStyle get selectionWidthStyle => _selectionPainter.selectionWidthStyle; set selectionWidthStyle(ui.BoxWidthStyle value) { _selectionPainter.selectionWidthStyle = value; } /// The object that controls the text selection, used by this render object /// for implementing cut, copy, and paste keyboard shortcuts. /// /// It will make cut, copy and paste functionality work with the most recently /// set [TextSelectionDelegate]. TextSelectionDelegate textSelectionDelegate; /// Track whether position of the start of the selected text is within the viewport. /// /// For example, if the text contains "Hello World", and the user selects /// "Hello", then scrolls so only "World" is visible, this will become false. /// If the user scrolls back so that the "H" is visible again, this will /// become true. /// /// This bool indicates whether the text is scrolled so that the handle is /// inside the text field viewport, as opposed to whether it is actually /// visible on the screen. ValueListenable get selectionStartInViewport => _selectionStartInViewport; final ValueNotifier _selectionStartInViewport = ValueNotifier( true, ); /// Track whether position of the end of the selected text is within the viewport. /// /// For example, if the text contains "Hello World", and the user selects /// "World", then scrolls so only "Hello" is visible, this will become /// 'false'. If the user scrolls back so that the "d" is visible again, this /// will become 'true'. /// /// This bool indicates whether the text is scrolled so that the handle is /// inside the text field viewport, as opposed to whether it is actually /// visible on the screen. ValueListenable get selectionEndInViewport => _selectionEndInViewport; final ValueNotifier _selectionEndInViewport = ValueNotifier(true); /// Returns the TextPosition above or below the given offset. TextPosition _getTextPositionVertical( TextPosition position, double verticalOffset, ) { final Offset caretOffset = _textPainter.getOffsetForCaret( position, _caretPrototype, ); final Offset caretOffsetTranslated = caretOffset.translate( 0.0, verticalOffset, ); return _textPainter.getPositionForOffset(caretOffsetTranslated); } // Start TextLayoutMetrics. /// {@macro flutter.services.TextLayoutMetrics.getLineAtOffset} @override TextSelection getLineAtOffset(TextPosition position) { final TextRange line = _textPainter.getLineBoundary(position); // If text is obscured, the entire string should be treated as one line. if (obscureText) { return TextSelection(baseOffset: 0, extentOffset: plainText.length); } return TextSelection(baseOffset: line.start, extentOffset: line.end); } /// {@macro flutter.painting.TextPainter.getWordBoundary} @override TextRange getWordBoundary(TextPosition position) { return _textPainter.getWordBoundary(position); } /// {@macro flutter.services.TextLayoutMetrics.getTextPositionAbove} @override TextPosition getTextPositionAbove(TextPosition position) { // The caret offset gives a location in the upper left hand corner of // the caret so the middle of the line above is a half line above that // point and the line below is 1.5 lines below that point. final double preferredLineHeight = _textPainter.preferredLineHeight; final double verticalOffset = -0.5 * preferredLineHeight; return _getTextPositionVertical(position, verticalOffset); } /// {@macro flutter.services.TextLayoutMetrics.getTextPositionBelow} @override TextPosition getTextPositionBelow(TextPosition position) { // The caret offset gives a location in the upper left hand corner of // the caret so the middle of the line above is a half line above that // point and the line below is 1.5 lines below that point. final double preferredLineHeight = _textPainter.preferredLineHeight; final double verticalOffset = 1.5 * preferredLineHeight; return _getTextPositionVertical(position, verticalOffset); } // End TextLayoutMetrics. void _updateSelectionExtentsVisibility(Offset effectiveOffset) { assert(selection != null); if (!selection!.isValid) { _selectionStartInViewport.value = false; _selectionEndInViewport.value = false; return; } final Rect visibleRegion = Offset.zero & size; final Offset startOffset = _textPainter.getOffsetForCaret( TextPosition(offset: selection!.start, affinity: selection!.affinity), _caretPrototype, ); // Check if the selection is visible with an approximation because a // difference between rounded and unrounded values causes the caret to be // reported as having a slightly (< 0.5) negative y offset. This rounding // happens in paragraph.cc's layout and TextPainter's // _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and // this can be changed to be a strict check instead of an approximation. const visibleRegionSlop = 0.5; _selectionStartInViewport.value = visibleRegion .inflate(visibleRegionSlop) .contains(startOffset + effectiveOffset); final Offset endOffset = _textPainter.getOffsetForCaret( TextPosition(offset: selection!.end, affinity: selection!.affinity), _caretPrototype, ); _selectionEndInViewport.value = visibleRegion .inflate(visibleRegionSlop) .contains(endOffset + effectiveOffset); } void _setTextEditingValue( TextEditingValue newValue, SelectionChangedCause cause, ) { textSelectionDelegate.userUpdateTextEditingValue(newValue, cause); } void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) { if (nextSelection.isValid) { // The nextSelection is calculated based on plainText, which can be out // of sync with the textSelectionDelegate.textEditingValue by one frame. // This is due to the render editable and editable text handle pointer // event separately. If the editable text changes the text during the // event handler, the render editable will use the outdated text stored in // the plainText when handling the pointer event. // // If this happens, we need to make sure the new selection is still valid. final int textLength = textSelectionDelegate.textEditingValue.text.length; nextSelection = nextSelection.copyWith( baseOffset: math.min(nextSelection.baseOffset, textLength), extentOffset: math.min(nextSelection.extentOffset, textLength), ); } _setTextEditingValue( textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection), cause, ); } @override void markNeedsPaint() { super.markNeedsPaint(); // Tell the painters to repaint since text layout may have changed. _foregroundRenderObject?.markNeedsPaint(); _backgroundRenderObject?.markNeedsPaint(); } @override void systemFontsDidChange() { super.systemFontsDidChange(); _textPainter.markNeedsLayout(); } /// Returns a plain text version of the text in [TextPainter]. /// /// If [obscureText] is true, returns the obscured text. See /// [obscureText] and [obscuringCharacter]. /// In order to get the styled text as an [InlineSpan] tree, use [text]. String get plainText => _textPainter.plainText; /// The text to paint in the form of a tree of [InlineSpan]s. /// /// In order to get the plain text representation, use [plainText]. InlineSpan? get text => _textPainter.text; final TextPainter _textPainter; AttributedString? _cachedAttributedValue; List? _cachedCombinedSemanticsInfos; set text(InlineSpan? value) { if (_textPainter.text == value) { return; } _cachedLineBreakCount = null; _textPainter.text = value; _cachedAttributedValue = null; _cachedCombinedSemanticsInfos = null; markNeedsLayout(); markNeedsSemanticsUpdate(); } TextPainter? _textIntrinsicsCache; TextPainter get _textIntrinsics { return (_textIntrinsicsCache ??= TextPainter()) ..text = _textPainter.text ..textAlign = _textPainter.textAlign ..textDirection = _textPainter.textDirection ..textScaler = _textPainter.textScaler ..maxLines = _textPainter.maxLines ..ellipsis = _textPainter.ellipsis ..locale = _textPainter.locale ..strutStyle = _textPainter.strutStyle ..textWidthBasis = _textPainter.textWidthBasis ..textHeightBehavior = _textPainter.textHeightBehavior; } /// How the text should be aligned horizontally. TextAlign get textAlign => _textPainter.textAlign; set textAlign(TextAlign value) { if (_textPainter.textAlign == value) { return; } _textPainter.textAlign = value; markNeedsLayout(); } /// The directionality of the text. /// /// This decides how the [TextAlign.start], [TextAlign.end], and /// [TextAlign.justify] values of [textAlign] are interpreted. /// /// This is also used to disambiguate how to render bidirectional text. For /// example, if the [text] is an English phrase followed by a Hebrew phrase, /// in a [TextDirection.ltr] context the English phrase will be on the left /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. // TextPainter.textDirection is nullable, but it is set to a // non-null value in the RenderEditable constructor and we refuse to // set it to null here, so _textPainter.textDirection cannot be null. TextDirection get textDirection => _textPainter.textDirection!; set textDirection(TextDirection value) { if (_textPainter.textDirection == value) { return; } _textPainter.textDirection = value; markNeedsLayout(); markNeedsSemanticsUpdate(); } /// Used by this renderer's internal [TextPainter] to select a locale-specific /// font. /// /// In some cases the same Unicode character may be rendered differently depending /// on the locale. For example the '骨' character is rendered differently in /// the Chinese and Japanese locales. In these cases the [locale] may be used /// to select a locale-specific font. /// /// If this value is null, a system-dependent algorithm is used to select /// the font. Locale? get locale => _textPainter.locale; set locale(Locale? value) { if (_textPainter.locale == value) { return; } _textPainter.locale = value; markNeedsLayout(); } /// The [StrutStyle] used by the renderer's internal [TextPainter] to /// determine the strut to use. StrutStyle? get strutStyle => _textPainter.strutStyle; set strutStyle(StrutStyle? value) { if (_textPainter.strutStyle == value) { return; } _textPainter.strutStyle = value; markNeedsLayout(); } /// The color to use when painting the cursor. Color? get cursorColor => _caretPainter.caretColor; set cursorColor(Color? value) { _caretPainter.caretColor = value; } /// The color to use when painting the cursor aligned to the text while /// rendering the floating cursor. /// /// Typically this would be set to [CupertinoColors.inactiveGray]. /// /// If this is null, the background cursor is not painted. /// /// See also: /// /// * [FloatingCursorDragState], which explains the floating cursor feature /// in detail. Color? get backgroundCursorColor => _caretPainter.backgroundCursorColor; set backgroundCursorColor(Color? value) { _caretPainter.backgroundCursorColor = value; } bool _disposeShowCursor; /// Whether to paint the cursor. ValueNotifier get showCursor => _showCursor; ValueNotifier _showCursor; set showCursor(ValueNotifier value) { if (_showCursor == value) { return; } if (attached) { _showCursor.removeListener(_showHideCursor); } if (_disposeShowCursor) { _showCursor.dispose(); _disposeShowCursor = false; } _showCursor = value; if (attached) { _showHideCursor(); _showCursor.addListener(_showHideCursor); } } void _showHideCursor() { _caretPainter.shouldPaint = showCursor.value; } /// Whether the editable is currently focused. bool get hasFocus => _hasFocus; bool _hasFocus = false; set hasFocus(bool value) { if (_hasFocus == value) { return; } _hasFocus = value; markNeedsSemanticsUpdate(); } /// Whether this rendering object will take a full line regardless the text width. bool get forceLine => _forceLine; bool _forceLine = false; set forceLine(bool value) { if (_forceLine == value) { return; } _forceLine = value; markNeedsLayout(); } /// Whether this rendering object is read only. bool get readOnly => _readOnly; bool _readOnly = false; set readOnly(bool value) { if (_readOnly == value) { return; } _readOnly = value; markNeedsSemanticsUpdate(); } /// The maximum number of lines for the text to span, wrapping if necessary. /// /// If this is 1 (the default), the text will not wrap, but will extend /// indefinitely instead. /// /// If this is null, there is no limit to the number of lines. /// /// When this is not null, the intrinsic height of the render object is the /// height of one line of text multiplied by this value. In other words, this /// also controls the height of the actual editing widget. int? get maxLines => _maxLines; int? _maxLines; /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int? value) { assert(value == null || value > 0); if (maxLines == value) { return; } _maxLines = value; // Special case maxLines == 1 to keep only the first line so we can get the // height of the first line in case there are hard line breaks in the text. // See the `_preferredHeight` method. _textPainter.maxLines = value == 1 ? 1 : null; markNeedsLayout(); } /// {@macro flutter.widgets.editableText.minLines} int? get minLines => _minLines; int? _minLines; /// The value may be null. If it is not null, then it must be greater than zero. set minLines(int? value) { assert(value == null || value > 0); if (minLines == value) { return; } _minLines = value; markNeedsLayout(); } /// {@macro flutter.widgets.editableText.expands} bool get expands => _expands; bool _expands; set expands(bool value) { if (expands == value) { return; } _expands = value; markNeedsLayout(); } /// The color to use when painting the selection. Color? get selectionColor => _selectionPainter.highlightColor; set selectionColor(Color? value) { _selectionPainter.highlightColor = value; } /// Deprecated. Will be removed in a future version of Flutter. Use /// [textScaler] instead. /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) double get textScaleFactor => _textPainter.textScaleFactor; @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) set textScaleFactor(double value) { textScaler = TextScaler.linear(value); } /// {@macro flutter.painting.textPainter.textScaler} TextScaler get textScaler => _textPainter.textScaler; set textScaler(TextScaler value) { if (_textPainter.textScaler == value) { return; } _textPainter.textScaler = value; markNeedsLayout(); } /// The region of text that is selected, if any. /// /// The caret position is represented by a collapsed selection. /// /// If [selection] is null, there is no selection and attempts to /// manipulate the selection will throw. TextSelection? get selection => _selection; TextSelection? _selection; set selection(TextSelection? value) { if (_selection == value) { return; } _selection = value; _selectionPainter.highlightedRange = value; markNeedsPaint(); markNeedsSemanticsUpdate(); } /// The offset at which the text should be painted. /// /// If the text content is larger than the editable line itself, the editable /// line clips the text. This property controls which part of the text is /// visible by shifting the text by the given offset before clipping. ViewportOffset get offset => _offset; ViewportOffset _offset; set offset(ViewportOffset value) { if (_offset == value) { return; } if (attached) { _offset.removeListener(markNeedsPaint); } _offset = value; if (attached) { _offset.addListener(markNeedsPaint); } markNeedsLayout(); } /// How thick the cursor will be. double get cursorWidth => _cursorWidth; double _cursorWidth = 1.0; set cursorWidth(double value) { if (_cursorWidth == value) { return; } _cursorWidth = value; markNeedsLayout(); } /// How tall the cursor will be. /// /// This can be null, in which case the getter will actually return [preferredLineHeight]. /// /// Setting this to itself fixes the value to the current [preferredLineHeight]. Setting /// this to null returns the behavior of deferring to [preferredLineHeight]. // TODO(ianh): This is a confusing API. We should have a separate getter for the effective cursor height. double get cursorHeight => _cursorHeight ?? preferredLineHeight; double? _cursorHeight; set cursorHeight(double? value) { if (_cursorHeight == value) { return; } _cursorHeight = value; markNeedsLayout(); } /// {@template flutter.rendering.RenderEditable.paintCursorAboveText} /// If the cursor should be painted on top of the text or underneath it. /// /// By default, the cursor should be painted on top for iOS platforms and /// underneath for Android platforms. /// {@endtemplate} bool get paintCursorAboveText => _paintCursorOnTop; bool _paintCursorOnTop; set paintCursorAboveText(bool value) { if (_paintCursorOnTop == value) { return; } _paintCursorOnTop = value; // Clear cached built-in painters and reconfigure painters. _cachedBuiltInForegroundPainters = null; _cachedBuiltInPainters = null; // Call update methods to rebuild and set the effective painters. _updateForegroundPainter(_foregroundPainter); _updatePainter(_painter); } /// {@template flutter.rendering.RenderEditable.cursorOffset} /// The offset that is used, in pixels, when painting the cursor on screen. /// /// By default, the cursor position should be set to an offset of /// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android /// platforms. The origin from where the offset is applied to is the arbitrary /// location where the cursor ends up being rendered from by default. /// {@endtemplate} Offset get cursorOffset => _caretPainter.cursorOffset; set cursorOffset(Offset value) { _caretPainter.cursorOffset = value; } /// How rounded the corners of the cursor should be. /// /// A null value is the same as [Radius.zero]. Radius? get cursorRadius => _caretPainter.cursorRadius; set cursorRadius(Radius? value) { _caretPainter.cursorRadius = value; } /// The [LayerLink] of start selection handle. /// /// [RenderEditable] is responsible for calculating the [Offset] of this /// [LayerLink], which will be used as [CompositedTransformTarget] of start handle. LayerLink get startHandleLayerLink => _startHandleLayerLink; LayerLink _startHandleLayerLink; set startHandleLayerLink(LayerLink value) { if (_startHandleLayerLink == value) { return; } _startHandleLayerLink = value; markNeedsPaint(); } /// The [LayerLink] of end selection handle. /// /// [RenderEditable] is responsible for calculating the [Offset] of this /// [LayerLink], which will be used as [CompositedTransformTarget] of end handle. LayerLink get endHandleLayerLink => _endHandleLayerLink; LayerLink _endHandleLayerLink; set endHandleLayerLink(LayerLink value) { if (_endHandleLayerLink == value) { return; } _endHandleLayerLink = value; markNeedsPaint(); } /// The padding applied to text field. Used to determine the bounds when /// moving the floating cursor. /// /// Defaults to a padding with left, top and right set to 4, bottom to 5. /// /// See also: /// /// * [FloatingCursorDragState], which explains the floating cursor feature /// in detail. EdgeInsets floatingCursorAddedMargin; /// Returns true if the floating cursor is visible, false otherwise. bool get floatingCursorOn => _floatingCursorOn; bool _floatingCursorOn = false; late TextPosition _floatingCursorTextPosition; /// Whether to allow the user to change the selection. /// /// Since [RenderEditable] does not handle selection manipulation /// itself, this actually only affects whether the accessibility /// hints provided to the system (via /// [describeSemanticsConfiguration]) will enable selection /// manipulation. It's the responsibility of this object's owner /// to provide selection manipulation affordances. /// /// This field is used by [selectionEnabled] (which then controls /// the accessibility hints mentioned above). When null, /// [obscureText] is used to determine the value of /// [selectionEnabled] instead. bool? get enableInteractiveSelection => _enableInteractiveSelection; bool? _enableInteractiveSelection; set enableInteractiveSelection(bool? value) { if (_enableInteractiveSelection == value) { return; } _enableInteractiveSelection = value; markNeedsLayout(); markNeedsSemanticsUpdate(); } /// Whether interactive selection are enabled based on the values of /// [enableInteractiveSelection] and [obscureText]. /// /// Since [RenderEditable] does not handle selection manipulation /// itself, this actually only affects whether the accessibility /// hints provided to the system (via /// [describeSemanticsConfiguration]) will enable selection /// manipulation. It's the responsibility of this object's owner /// to provide selection manipulation affordances. /// /// By default, [enableInteractiveSelection] is null, [obscureText] is false, /// and this getter returns true. /// /// If [enableInteractiveSelection] is null and [obscureText] is true, then this /// getter returns false. This is the common case for password fields. /// /// If [enableInteractiveSelection] is non-null then its value is /// returned. An application might [enableInteractiveSelection] to /// true to enable interactive selection for a password field, or to /// false to unconditionally disable interactive selection. bool get selectionEnabled { return enableInteractiveSelection ?? !obscureText; } /// The color used to paint the prompt rectangle. /// /// The prompt rectangle will only be requested on non-web iOS applications. // TODO(ianh): We should change the getter to return null when _promptRectRange is null // (otherwise, if you set it to null and then get it, you get back non-null). // Alternatively, we could stop supporting setting this to null. Color? get promptRectColor => _autocorrectHighlightPainter.highlightColor; set promptRectColor(Color? newValue) { _autocorrectHighlightPainter.highlightColor = newValue; } /// Dismisses the currently displayed prompt rectangle and displays a new prompt rectangle /// over [newRange] in the given color [promptRectColor]. /// /// The prompt rectangle will only be requested on non-web iOS applications. /// /// When set to null, the currently displayed prompt rectangle (if any) will be dismissed. // ignore: use_setters_to_change_properties, (API predates enforcing the lint) void setPromptRectRange(TextRange? newRange) { _autocorrectHighlightPainter.highlightedRange = newRange; } /// The maximum amount the text is allowed to scroll. /// /// This value is only valid after layout and can change as additional /// text is entered or removed in order to accommodate expanding when /// [expands] is set to true. double get maxScrollExtent => _maxScrollExtent; double _maxScrollExtent = 0; double get _caretMargin => _kCaretGap + cursorWidth; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { if (value != _clipBehavior) { _clipBehavior = value; markNeedsPaint(); markNeedsSemanticsUpdate(); } } /// Collected during [describeSemanticsConfiguration], used by /// [assembleSemanticsNode]. List? _semanticsInfo; // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they // can be re-used when [assembleSemanticsNode] is called again. This ensures // stable ids for the [SemanticsNode]s of [TextSpan]s across // [assembleSemanticsNode] invocations. Map? _cachedChildNodes; /// Returns a list of rects that bound the given selection, and the text /// direction. The text direction is used by the engine to calculate /// the closest position to a given point. /// /// See [TextPainter.getBoxesForSelection] for more details. List getBoxesForSelection(TextSelection selection) { _computeTextMetricsIfNeeded(); return _textPainter .getBoxesForSelection( selection, boxHeightStyle: selectionHeightStyle, boxWidthStyle: selectionWidthStyle, ) .map( (TextBox textBox) => TextBox.fromLTRBD( textBox.left + _paintOffset.dx, textBox.top + _paintOffset.dy, textBox.right + _paintOffset.dx, textBox.bottom + _paintOffset.dy, textBox.direction, ), ) .toList(); } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); _semanticsInfo = _textPainter.text!.getSemanticsInformation(); // TODO(chunhtai): the macOS does not provide a public API to support text // selections across multiple semantics nodes. Remove this platform check // once we can support it. // https://github.com/flutter/flutter/issues/77957 if (_semanticsInfo!.any( (InlineSpanSemanticsInformation info) => info.recognizer != null, ) && defaultTargetPlatform != TargetPlatform.macOS) { // assert(readOnly && !obscureText); // For Selectable rich text with recognizer, we need to create a semantics // node for each text fragment. config ..isSemanticBoundary = true ..explicitChildNodes = true; return; } if (_cachedAttributedValue == null) { if (obscureText) { _cachedAttributedValue = AttributedString( obscuringCharacter * plainText.length, ); } else { final buffer = StringBuffer(); var offset = 0; final attributes = []; for (final InlineSpanSemanticsInformation info in _semanticsInfo!) { final String label = info.semanticsLabel ?? info.text; for (final StringAttribute infoAttribute in info.stringAttributes) { final TextRange originalRange = infoAttribute.range; attributes.add( infoAttribute.copy( range: TextRange( start: offset + originalRange.start, end: offset + originalRange.end, ), ), ); } buffer.write(label); offset += label.length; } _cachedAttributedValue = AttributedString( buffer.toString(), attributes: attributes, ); } } config ..attributedValue = _cachedAttributedValue! ..isObscured = obscureText ..isMultiline = _isMultiline ..textDirection = textDirection ..isFocused = hasFocus ..isFocusable = true ..isTextField = true ..isReadOnly = readOnly // This is the default for customer that uses RenderEditable directly. // The real value is typically set by EditableText. ..inputType = ui.SemanticsInputType.text; if (hasFocus && selectionEnabled) { config.onSetSelection = _handleSetSelection; } if (hasFocus && !readOnly) { config.onSetText = _handleSetText; } if (selectionEnabled && (selection?.isValid ?? false)) { config.textSelection = selection; if (_textPainter.getOffsetBefore(selection!.extentOffset) != null) { config ..onMoveCursorBackwardByWord = _handleMoveCursorBackwardByWord ..onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter; } if (_textPainter.getOffsetAfter(selection!.extentOffset) != null) { config ..onMoveCursorForwardByWord = _handleMoveCursorForwardByWord ..onMoveCursorForwardByCharacter = _handleMoveCursorForwardByCharacter; } } } void _handleSetText(String text) { textSelectionDelegate.userUpdateTextEditingValue( TextEditingValue( text: text, selection: TextSelection.collapsed(offset: text.length), ), SelectionChangedCause.keyboard, ); } @override void assembleSemanticsNode( SemanticsNode node, SemanticsConfiguration config, Iterable children, ) { assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty); final newChildren = []; TextDirection currentDirection = textDirection; Rect currentRect; var ordinal = 0.0; var start = 0; var placeholderIndex = 0; var childIndex = 0; RenderBox? child = firstChild; final newChildCache = {}; _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) { final selection = TextSelection( baseOffset: start, extentOffset: start + info.text.length, ); start += info.text.length; if (info.isPlaceholder) { // A placeholder span may have 0 to multiple semantics nodes, we need // to annotate all of the semantics nodes belong to this span. while (children.length > childIndex && children .elementAt(childIndex) .isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) { final SemanticsNode childNode = children.elementAt(childIndex); final parentData = child!.parentData! as TextParentData; assert(parentData.offset != null); newChildren.add(childNode); childIndex += 1; } child = childAfter(child!); placeholderIndex += 1; } else { final initialDirection = currentDirection; final List rects = _textPainter.getBoxesForSelection( selection, ); if (rects.isEmpty) { continue; } Rect rect = rects.first.toRect(); currentDirection = rects.first.direction; for (final ui.TextBox textBox in rects.skip(1)) { rect = rect.expandToInclude(textBox.toRect()); currentDirection = textBox.direction; } // Any of the text boxes may have had infinite dimensions. // We shouldn't pass infinite dimensions up to the bridges. rect = Rect.fromLTWH( math.max(0.0, rect.left), math.max(0.0, rect.top), math.min(rect.width, constraints.maxWidth), math.min(rect.height, constraints.maxHeight), ); // Round the current rectangle to make this API testable and add some // padding so that the accessibility rects do not overlap with the text. currentRect = Rect.fromLTRB( rect.left.floorToDouble() - 4.0, rect.top.floorToDouble() - 4.0, rect.right.ceilToDouble() + 4.0, rect.bottom.ceilToDouble() + 4.0, ); final configuration = SemanticsConfiguration() ..sortKey = OrdinalSortKey(ordinal++) ..textDirection = initialDirection ..attributedLabel = AttributedString( info.semanticsLabel ?? info.text, attributes: info.stringAttributes, ); switch (info.recognizer) { case TapGestureRecognizer(onTap: final VoidCallback? handler): case DoubleTapGestureRecognizer( onDoubleTap: final VoidCallback? handler, ): if (handler != null) { configuration ..onTap = handler ..isLink = true; } case LongPressGestureRecognizer( onLongPress: final GestureLongPressCallback? onLongPress, ): if (onLongPress != null) { configuration.onLongPress = onLongPress; } case null: break; default: assert(false, '${info.recognizer.runtimeType} is not supported.'); } if (node.parentPaintClipRect != null) { final Rect paintRect = node.parentPaintClipRect!.intersect( currentRect, ); configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty; } late final SemanticsNode newChild; if (_cachedChildNodes?.isNotEmpty ?? false) { newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!; } else { final key = UniqueKey(); newChild = SemanticsNode( key: key, showOnScreen: _createShowOnScreenFor(key), ); } newChild ..updateWith(config: configuration) ..rect = currentRect; newChildCache[newChild.key!] = newChild; newChildren.add(newChild); } } _cachedChildNodes = newChildCache; node.updateWith(config: config, childrenInInversePaintOrder: newChildren); } VoidCallback? _createShowOnScreenFor(Key key) { return () { final SemanticsNode node = _cachedChildNodes![key]!; showOnScreen(descendant: this, rect: node.rect); }; } // TODO(ianh): in theory, [selection] could become null between when // we last called describeSemanticsConfiguration and when the // callbacks are invoked, in which case the callbacks will crash... void _handleSetSelection(TextSelection selection) { _setSelection(selection, SelectionChangedCause.keyboard); } void _handleMoveCursorForwardByCharacter(bool extendSelection) { assert(selection != null); final int? extentOffset = _textPainter.getOffsetAfter( selection!.extentOffset, ); if (extentOffset == null) { return; } final int baseOffset = !extendSelection ? extentOffset : selection!.baseOffset; _setSelection( TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard, ); } void _handleMoveCursorBackwardByCharacter(bool extendSelection) { assert(selection != null); final int? extentOffset = _textPainter.getOffsetBefore( selection!.extentOffset, ); if (extentOffset == null) { return; } final int baseOffset = !extendSelection ? extentOffset : selection!.baseOffset; _setSelection( TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard, ); } void _handleMoveCursorForwardByWord(bool extendSelection) { assert(selection != null); final TextRange currentWord = _textPainter.getWordBoundary( selection!.extent, ); final TextRange? nextWord = _getNextWord(currentWord.end); if (nextWord == null) { return; } final int baseOffset = extendSelection ? selection!.baseOffset : nextWord.start; _setSelection( TextSelection(baseOffset: baseOffset, extentOffset: nextWord.start), SelectionChangedCause.keyboard, ); } void _handleMoveCursorBackwardByWord(bool extendSelection) { assert(selection != null); final TextRange currentWord = _textPainter.getWordBoundary( selection!.extent, ); final TextRange? previousWord = _getPreviousWord(currentWord.start - 1); if (previousWord == null) { return; } final int baseOffset = extendSelection ? selection!.baseOffset : previousWord.start; _setSelection( TextSelection(baseOffset: baseOffset, extentOffset: previousWord.start), SelectionChangedCause.keyboard, ); } TextRange? _getNextWord(int offset) { while (true) { final TextRange range = _textPainter.getWordBoundary( TextPosition(offset: offset), ); if (!range.isValid || range.isCollapsed) { return null; } if (!_onlyWhitespace(range)) { return range; } offset = range.end; } } TextRange? _getPreviousWord(int offset) { while (offset >= 0) { final TextRange range = _textPainter.getWordBoundary( TextPosition(offset: offset), ); if (!range.isValid || range.isCollapsed) { return null; } if (!_onlyWhitespace(range)) { return range; } offset = range.start - 1; } return null; } // Check if the given text range only contains white space or separator // characters. // // Includes newline characters from ASCII and separators from the // [unicode separator category](https://www.compart.com/en/unicode/category/Zs) // TODO(zanderso): replace when we expose this ICU information. bool _onlyWhitespace(TextRange range) { for (int i = range.start; i < range.end; i++) { final int codeUnit = text!.codeUnitAt(i)!; if (!TextLayoutMetrics.isWhitespace(codeUnit)) { return false; } } return true; } @override void attach(PipelineOwner owner) { super.attach(owner); _foregroundRenderObject?.attach(owner); _backgroundRenderObject?.attach(owner); _tap = TapGestureRecognizer(debugOwner: this) ..onTapDown = _handleTapDown ..onTap = _handleTap; _longPress = LongPressGestureRecognizer(debugOwner: this) ..onLongPress = _handleLongPress; _offset.addListener(markNeedsPaint); _showHideCursor(); _showCursor.addListener(_showHideCursor); } @override void detach() { _tap.dispose(); _longPress.dispose(); _offset.removeListener(markNeedsPaint); _showCursor.removeListener(_showHideCursor); super.detach(); _foregroundRenderObject?.detach(); _backgroundRenderObject?.detach(); } @override void redepthChildren() { final RenderObject? foregroundChild = _foregroundRenderObject; final RenderObject? backgroundChild = _backgroundRenderObject; if (foregroundChild != null) { redepthChild(foregroundChild); } if (backgroundChild != null) { redepthChild(backgroundChild); } super.redepthChildren(); } @override void visitChildren(RenderObjectVisitor visitor) { final RenderObject? foregroundChild = _foregroundRenderObject; final RenderObject? backgroundChild = _backgroundRenderObject; if (foregroundChild != null) { visitor(foregroundChild); } if (backgroundChild != null) { visitor(backgroundChild); } super.visitChildren(visitor); } bool get _isMultiline => maxLines != 1; Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal; Offset get _paintOffset => switch (_viewportAxis) { Axis.horizontal => Offset(-offset.pixels, 0.0), Axis.vertical => Offset(0.0, -offset.pixels), }; double get _viewportExtent { assert(hasSize); return switch (_viewportAxis) { Axis.horizontal => size.width, Axis.vertical => size.height, }; } double _getMaxScrollExtent(Size contentSize) { assert(hasSize); return switch (_viewportAxis) { Axis.horizontal => math.max(0.0, contentSize.width - size.width), Axis.vertical => math.max(0.0, contentSize.height - size.height), }; } // We need to check the paint offset here because during animation, the start of // the text may position outside the visible region even when the text fits. bool get _hasVisualOverflow => _maxScrollExtent > 0 || _paintOffset != Offset.zero; /// Returns the local coordinates of the endpoints of the given selection. /// /// If the selection is collapsed (and therefore occupies a single point), the /// returned list is of length one. Otherwise, the selection is not collapsed /// and the returned list is of length two. In this case, however, the two /// points might actually be co-located (e.g., because of a bidirectional /// selection that contains some text but whose ends meet in the middle). /// /// See also: /// /// * [getLocalRectForCaret], which is the equivalent but for /// a [TextPosition] rather than a [TextSelection]. List getEndpointsForSelection(TextSelection selection) { _computeTextMetricsIfNeeded(); final Offset paintOffset = _paintOffset; final List boxes = selection.isCollapsed ? [] : _textPainter.getBoxesForSelection( selection, boxHeightStyle: selectionHeightStyle, boxWidthStyle: selectionWidthStyle, ); if (boxes.isEmpty) { // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary. final Offset caretOffset = _textPainter.getOffsetForCaret( selection.extent, _caretPrototype, ); final Offset start = Offset(0.0, preferredLineHeight) + caretOffset + paintOffset; return [TextSelectionPoint(start, null)]; } else { final Offset start = Offset( clampDouble(boxes.first.start, 0, _textPainter.size.width), boxes.first.bottom, ) + paintOffset; final Offset end = Offset( clampDouble(boxes.last.end, 0, _textPainter.size.width), boxes.last.bottom, ) + paintOffset; return [ TextSelectionPoint(start, boxes.first.direction), TextSelectionPoint(end, boxes.last.direction), ]; } } /// Returns the smallest [Rect], in the local coordinate system, that covers /// the text within the [TextRange] specified. /// /// This method is used to calculate the approximate position of the IME bar /// on iOS. /// /// Returns null if [TextRange.isValid] is false for the given `range`, or the /// given `range` is collapsed. Rect? getRectForComposingRange(TextRange range) { if (!range.isValid || range.isCollapsed) { return null; } _computeTextMetricsIfNeeded(); final List boxes = _textPainter.getBoxesForSelection( TextSelection(baseOffset: range.start, extentOffset: range.end), boxHeightStyle: selectionHeightStyle, boxWidthStyle: selectionWidthStyle, ); return boxes .fold( null, (Rect? accum, TextBox incoming) => accum?.expandToInclude(incoming.toRect()) ?? incoming.toRect(), ) ?.shift(_paintOffset); } /// Returns the position in the text for the given global coordinate. /// /// See also: /// /// * [getLocalRectForCaret], which is the reverse operation, taking /// a [TextPosition] and returning a [Rect]. /// * [TextPainter.getPositionForOffset], which is the equivalent method /// for a [TextPainter] object. TextPosition getPositionForPoint(Offset globalPosition) { _computeTextMetricsIfNeeded(); return _textPainter.getPositionForOffset( globalToLocal(globalPosition) - _paintOffset, ); } /// Returns the [Rect] in local coordinates for the caret at the given text /// position. /// /// See also: /// /// * [getPositionForPoint], which is the reverse operation, taking /// an [Offset] in global coordinates and returning a [TextPosition]. /// * [getEndpointsForSelection], which is the equivalent but for /// a selection rather than a particular text position. /// * [TextPainter.getOffsetForCaret], the equivalent method for a /// [TextPainter] object. Rect getLocalRectForCaret(TextPosition caretPosition) { _computeTextMetricsIfNeeded(); final Rect caretPrototype = _caretPrototype; final Offset caretOffset = _textPainter.getOffsetForCaret( caretPosition, caretPrototype, ); Rect caretRect = caretPrototype.shift(caretOffset + cursorOffset); final double scrollableWidth = math.max( _textPainter.width + _caretMargin, size.width, ); final double caretX = clampDouble( caretRect.left, 0, math.max(scrollableWidth - _caretMargin, 0), ); caretRect = Offset(caretX, caretRect.top) & caretRect.size; final double fullHeight = _textPainter.getFullHeightForCaret( caretPosition, caretPrototype, ); switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: // Center the caret vertically along the text. final double heightDiff = fullHeight - caretRect.height; caretRect = Rect.fromLTWH( caretRect.left, caretRect.top + heightDiff / 2, caretRect.width, caretRect.height, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: // Override the height to take the full height of the glyph at the TextPosition // when not on iOS. iOS has special handling that creates a taller caret. // TODO(garyq): see https://github.com/flutter/flutter/issues/120836. final double caretHeight = cursorHeight; // Center the caret vertically along the text. final double heightDiff = fullHeight - caretHeight; caretRect = Rect.fromLTWH( caretRect.left, caretRect.top - _kCaretHeightOffset + heightDiff / 2, caretRect.width, caretHeight, ); } caretRect = caretRect.shift(_paintOffset); return caretRect.shift(_snapToPhysicalPixel(caretRect.topLeft)); } @override double computeMinIntrinsicWidth(double height) { final List placeholderDimensions = layoutInlineChildren( double.infinity, (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0), ChildLayoutHelper.getDryBaseline, ); final (double minWidth, double maxWidth) = _adjustConstraints(); return (_textIntrinsics ..setPlaceholderDimensions(placeholderDimensions) ..layout(minWidth: minWidth, maxWidth: maxWidth)) .minIntrinsicWidth; } @override double computeMaxIntrinsicWidth(double height) { final List placeholderDimensions = layoutInlineChildren( double.infinity, // Height and baseline is irrelevant as all text will be laid // out in a single line. Therefore, using 0.0 as a dummy for the height. (RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0), ChildLayoutHelper.getDryBaseline, ); final (double minWidth, double maxWidth) = _adjustConstraints(); return (_textIntrinsics ..setPlaceholderDimensions(placeholderDimensions) ..layout(minWidth: minWidth, maxWidth: maxWidth)) .maxIntrinsicWidth + _caretMargin; } /// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight]. /// This does not require the layout to be updated. double get preferredLineHeight => _textPainter.preferredLineHeight; int? _cachedLineBreakCount; int _countHardLineBreaks(String text) { final int? cachedValue = _cachedLineBreakCount; if (cachedValue != null) { return cachedValue; } var count = 0; for (var index = 0; index < text.length; index += 1) { switch (text.codeUnitAt(index)) { case 0x000A: // LF case 0x0085: // NEL case 0x000B: // VT case 0x000C: // FF, treating it as a regular line separator case 0x2028: // LS case 0x2029: // PS count += 1; } } return _cachedLineBreakCount = count; } double _preferredHeight(double width) { final int? maxLines = this.maxLines; final int? minLines = this.minLines ?? maxLines; final double minHeight = preferredLineHeight * (minLines ?? 0); assert(maxLines != 1 || _textIntrinsics.maxLines == 1); if (maxLines == null) { final double estimatedHeight; if (width == double.infinity) { estimatedHeight = preferredLineHeight * (_countHardLineBreaks(plainText) + 1); } else { final (double minWidth, double maxWidth) = _adjustConstraints( maxWidth: width, ); estimatedHeight = (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)) .height; } return math.max(estimatedHeight, minHeight); } // Special case maxLines == 1 since it forces the scrollable direction // to be horizontal. Report the real height to prevent the text from being // clipped. if (maxLines == 1) { // The _layoutText call lays out the paragraph using infinite width when // maxLines == 1. Also _textPainter.maxLines will be set to 1 so should // there be any line breaks only the first line is shown. final (double minWidth, double maxWidth) = _adjustConstraints( maxWidth: width, ); return (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)) .height; } if (minLines == maxLines) { return minHeight; } final double maxHeight = preferredLineHeight * maxLines; final (double minWidth, double maxWidth) = _adjustConstraints( maxWidth: width, ); return clampDouble( (_textIntrinsics..layout(minWidth: minWidth, maxWidth: maxWidth)).height, minHeight, maxHeight, ); } @override double computeMinIntrinsicHeight(double width) => getMaxIntrinsicHeight(width); @override double computeMaxIntrinsicHeight(double width) { _textIntrinsics.setPlaceholderDimensions( layoutInlineChildren( width, ChildLayoutHelper.dryLayoutChild, ChildLayoutHelper.getDryBaseline, ), ); return _preferredHeight(width); } @override double computeDistanceToActualBaseline(TextBaseline baseline) { _computeTextMetricsIfNeeded(); return _textPainter.computeDistanceToActualBaseline(baseline); } @override bool hitTestSelf(Offset position) => true; @override @protected bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { final Offset effectivePosition = position - _paintOffset; final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset( effectivePosition, ); // The hit-test can't fall through the horizontal gaps between visually // adjacent characters on the same line, even with a large letter-spacing or // text justification, as graphemeClusterLayoutBounds.width is the advance // width to the next character, so there's no gap between their // graphemeClusterLayoutBounds rects. final InlineSpan? spanHit = glyph != null && glyph.graphemeClusterLayoutBounds.contains(effectivePosition) ? _textPainter.text!.getSpanForPosition( TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start), ) : null; switch (spanHit) { case final HitTestTarget span: result.add(HitTestEntry(span)); return true; case _: return hitTestInlineChildren(result, effectivePosition); } } late TapGestureRecognizer _tap; late LongPressGestureRecognizer _longPress; @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent) { assert(!debugNeedsLayout); if (!ignorePointer) { // Propagates the pointer event to selection handlers. _tap.addPointer(event); _longPress.addPointer(event); } } } Offset? _lastTapDownPosition; Offset? _lastSecondaryTapDownPosition; /// {@template flutter.rendering.RenderEditable.lastSecondaryTapDownPosition} /// The position of the most recent secondary tap down event on this text /// input. /// {@endtemplate} Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition; /// Tracks the position of a secondary tap event. /// /// Should be called before attempting to change the selection based on the /// position of a secondary tap. void handleSecondaryTapDown(TapDownDetails details) { _lastTapDownPosition = details.globalPosition; _lastSecondaryTapDownPosition = details.globalPosition; } /// If [ignorePointer] is false (the default) then this method is called by /// the internal gesture recognizer's [TapGestureRecognizer.onTapDown] /// callback. /// /// When [ignorePointer] is true, an ancestor widget must respond to tap /// down events by calling this method. void handleTapDown(TapDownDetails details) { _lastTapDownPosition = details.globalPosition; } void _handleTapDown(TapDownDetails details) { assert(!ignorePointer); handleTapDown(details); } /// If [ignorePointer] is false (the default) then this method is called by /// the internal gesture recognizer's [TapGestureRecognizer.onTap] /// callback. /// /// When [ignorePointer] is true, an ancestor widget must respond to tap /// events by calling this method. void handleTap() { selectPosition(cause: SelectionChangedCause.tap); } void _handleTap() { assert(!ignorePointer); handleTap(); } /// If [ignorePointer] is false (the default) then this method is called by /// the internal gesture recognizer's [DoubleTapGestureRecognizer.onDoubleTap] /// callback. /// /// When [ignorePointer] is true, an ancestor widget must respond to double /// tap events by calling this method. void handleDoubleTap() { selectWord(cause: SelectionChangedCause.doubleTap); } /// If [ignorePointer] is false (the default) then this method is called by /// the internal gesture recognizer's [LongPressGestureRecognizer.onLongPress] /// callback. /// /// When [ignorePointer] is true, an ancestor widget must respond to long /// press events by calling this method. void handleLongPress() { selectWord(cause: SelectionChangedCause.longPress); } void _handleLongPress() { assert(!ignorePointer); handleLongPress(); } /// Move selection to the location of the last tap down. /// /// {@template flutter.rendering.RenderEditable.selectPosition} /// This method is mainly used to translate user inputs in global positions /// into a [TextSelection]. When used in conjunction with a [EditableText], /// the selection change is fed back into [TextEditingController.selection]. /// /// If you have a [TextEditingController], it's generally easier to /// programmatically manipulate its `value` or `selection` directly. /// {@endtemplate} void selectPosition({required SelectionChangedCause cause}) { selectPositionAt(from: _lastTapDownPosition!, cause: cause); } /// Select text between the global positions [from] and [to]. /// /// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds /// to the [TextSelection.extentOffset]. void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause, }) { _computeTextMetricsIfNeeded(); final localFrom = globalToLocal(from); final TextPosition fromPosition = _textPainter.getPositionForOffset( localFrom - _paintOffset, ); final TextPosition? toPosition = to == null ? null : _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset); int baseOffset = fromPosition.offset; int extentOffset = toPosition?.offset ?? fromPosition.offset; // bggRGjQaUbCoE tap if (toPosition == null) { baseOffset = controller.tapOffset( baseOffset, textPainter: _textPainter, localPos: localFrom, lastTapDownPosition: from, ); extentOffset = baseOffset; } else { // select final isNormalized = baseOffset < extentOffset; final newOffset = controller.longPressOffset( isNormalized ? baseOffset : extentOffset, isNormalized ? extentOffset : baseOffset, ); baseOffset = isNormalized ? newOffset.startOffset : newOffset.endOffset; extentOffset = isNormalized ? newOffset.endOffset : newOffset.startOffset; } final newSelection = TextSelection( baseOffset: baseOffset, extentOffset: extentOffset, affinity: fromPosition.affinity, ); _setSelection(newSelection, cause); } /// {@macro flutter.painting.TextPainter.wordBoundaries} WordBoundary get wordBoundaries => _textPainter.wordBoundaries; /// Select a word around the location of the last tap down. /// /// {@macro flutter.rendering.RenderEditable.selectPosition} void selectWord({required SelectionChangedCause cause}) { selectWordsInRange(from: _lastTapDownPosition!, cause: cause); } /// Selects the set words of a paragraph that intersect a given range of global positions. /// /// The set of words selected are not strictly bounded by the range of global positions. /// /// The first and last endpoints of the selection will always be at the /// beginning and end of a word respectively. /// /// {@macro flutter.rendering.RenderEditable.selectPosition} void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause, }) { _computeTextMetricsIfNeeded(); final TextPosition fromPosition = _textPainter.getPositionForOffset( globalToLocal(from) - _paintOffset, ); final TextSelection fromWord = getWordAtOffset(fromPosition); final TextPosition toPosition = to == null ? fromPosition : _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset); final TextSelection toWord = toPosition == fromPosition ? fromWord : getWordAtOffset(toPosition); final bool isFromWordBeforeToWord = fromWord.start < toWord.end; // bggRGjQaUbCoE longpress var startOffset = isFromWordBeforeToWord ? fromWord.baseOffset : toWord.baseOffset; var endOffset = isFromWordBeforeToWord ? toWord.extentOffset : fromWord.extentOffset; final newOffset = controller.longPressOffset(startOffset, endOffset); startOffset = newOffset.startOffset; endOffset = newOffset.endOffset; _setSelection( TextSelection( baseOffset: isFromWordBeforeToWord ? startOffset : endOffset, extentOffset: isFromWordBeforeToWord ? endOffset : startOffset, affinity: fromWord.affinity, ), cause, ); } /// Move the selection to the beginning or end of a word. /// /// {@macro flutter.rendering.RenderEditable.selectPosition} void selectWordEdge({required SelectionChangedCause cause}) { _computeTextMetricsIfNeeded(); assert(_lastTapDownPosition != null); final localPos = globalToLocal(_lastTapDownPosition!); TextPosition position = _textPainter.getPositionForOffset( localPos - _paintOffset, ); // bggRGjQaUbCoE ios tap final newOffset = controller.tapOffset( position.offset, textPainter: _textPainter, localPos: localPos, lastTapDownPosition: _lastTapDownPosition!, ); final newSelection = TextSelection.collapsed(offset: newOffset); // final TextRange word = _textPainter.getWordBoundary(position); // late TextSelection newSelection; // if (position.offset <= word.start) { // newSelection = TextSelection.collapsed(offset: word.start); // } else { // newSelection = TextSelection.collapsed( // offset: word.end, // affinity: TextAffinity.upstream, // ); // } _setSelection(newSelection, cause); } /// Returns a [TextSelection] that encompasses the word at the given /// [TextPosition]. @visibleForTesting TextSelection getWordAtOffset(TextPosition position) { // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= plainText.length) { return TextSelection.fromPosition( TextPosition(offset: plainText.length, affinity: TextAffinity.upstream), ); } // If text is obscured, the entire sentence should be treated as one word. if (obscureText) { return TextSelection(baseOffset: 0, extentOffset: plainText.length); } final TextRange word = _textPainter.getWordBoundary(position); final int effectiveOffset; switch (position.affinity) { case TextAffinity.upstream: // upstream affinity is effectively -1 in text position. effectiveOffset = position.offset - 1; case TextAffinity.downstream: effectiveOffset = position.offset; } assert(effectiveOffset >= 0); // On iOS, select the previous word if there is a previous word, or select // to the end of the next word if there is a next word. Select nothing if // there is neither a previous word nor a next word. // // If the platform is Android and the text is read only, try to select the // previous word if there is one; otherwise, select the single whitespace at // the position. if (effectiveOffset > 0 && TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset))) { final TextRange? previousWord = _getPreviousWord(word.start); switch (defaultTargetPlatform) { case TargetPlatform.iOS: if (previousWord == null) { final TextRange? nextWord = _getNextWord(word.start); if (nextWord == null) { return TextSelection.collapsed(offset: position.offset); } return TextSelection( baseOffset: position.offset, extentOffset: nextWord.end, ); } return TextSelection( baseOffset: previousWord.start, extentOffset: position.offset, ); case TargetPlatform.android: if (readOnly) { if (previousWord == null) { return TextSelection( baseOffset: position.offset, extentOffset: position.offset + 1, ); } return TextSelection( baseOffset: previousWord.start, extentOffset: position.offset, ); } case TargetPlatform.fuchsia: case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: break; } } return TextSelection(baseOffset: word.start, extentOffset: word.end); } // Placeholder dimensions representing the sizes of child inline widgets. // // These need to be cached because the text painter's placeholder dimensions // will be overwritten during intrinsic width/height calculations and must be // restored to the original values before final layout and painting. List? _placeholderDimensions; (double minWidth, double maxWidth) _adjustConstraints({ double minWidth = 0.0, double maxWidth = double.infinity, }) { final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin); final double availableMinWidth = math.min(minWidth, availableMaxWidth); return ( forceLine ? availableMaxWidth : availableMinWidth, _isMultiline ? availableMaxWidth : double.infinity, ); } // Computes the text metrics if `_textPainter`'s layout information was marked // as dirty. // // This method must be called in `RenderEditable`'s public methods that expose // `_textPainter`'s metrics. For instance, `systemFontsDidChange` sets // _textPainter._paragraph to null, so accessing _textPainter's metrics // immediately after `systemFontsDidChange` without first calling this method // may crash. // // This method is also called in various paint methods (`RenderEditable.paint` // as well as its foreground/background painters' `paint`). It's needed // because invisible render objects kept in the tree by `KeepAlive` may not // get a chance to do layout but can still paint. // See https://github.com/flutter/flutter/issues/84896. // // This method only re-computes layout if the underlying `_textPainter`'s // layout cache is invalidated (by calling `TextPainter.markNeedsLayout`), or // the constraints used to layout the `_textPainter` is different. See // `TextPainter.layout`. void _computeTextMetricsIfNeeded() { final (double minWidth, double maxWidth) = _adjustConstraints( minWidth: constraints.minWidth, maxWidth: constraints.maxWidth, ); _textPainter.layout(minWidth: minWidth, maxWidth: maxWidth); } late Rect _caretPrototype; // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/120836 // /// On iOS, the cursor is taller than the cursor on Android. The height /// of the cursor for iOS is approximate and obtained through an eyeball /// comparison. void _computeCaretPrototype() { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: _caretPrototype = Rect.fromLTRB( 0.0, 0.0, cursorWidth, cursorHeight + 2, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: _caretPrototype = Rect.fromLTWH( 0.0, _kCaretHeightOffset, cursorWidth, cursorHeight - 2.0 * _kCaretHeightOffset, ); } } // Computes the offset to apply to the given [sourceOffset] so it perfectly // snaps to physical pixels. Offset _snapToPhysicalPixel(Offset sourceOffset) { final Offset globalOffset = localToGlobal(sourceOffset); final double pixelMultiple = 1.0 / _devicePixelRatio; return Offset( globalOffset.dx.isFinite ? (globalOffset.dx / pixelMultiple).round() * pixelMultiple - globalOffset.dx : 0, globalOffset.dy.isFinite ? (globalOffset.dy / pixelMultiple).round() * pixelMultiple - globalOffset.dy : 0, ); } @override @protected Size computeDryLayout(covariant BoxConstraints constraints) { final (double minWidth, double maxWidth) = _adjustConstraints( minWidth: constraints.minWidth, maxWidth: constraints.maxWidth, ); _textIntrinsics ..setPlaceholderDimensions( layoutInlineChildren( constraints.maxWidth, ChildLayoutHelper.dryLayoutChild, ChildLayoutHelper.getDryBaseline, ), ) ..layout(minWidth: minWidth, maxWidth: maxWidth); final double width = forceLine ? constraints.maxWidth : constraints.constrainWidth(_textIntrinsics.size.width + _caretMargin); return Size( width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)), ); } @override double computeDryBaseline( covariant BoxConstraints constraints, TextBaseline baseline, ) { final (double minWidth, double maxWidth) = _adjustConstraints( minWidth: constraints.minWidth, maxWidth: constraints.maxWidth, ); _textIntrinsics ..setPlaceholderDimensions( layoutInlineChildren( constraints.maxWidth, ChildLayoutHelper.dryLayoutChild, ChildLayoutHelper.getDryBaseline, ), ) ..layout(minWidth: minWidth, maxWidth: maxWidth); return _textIntrinsics.computeDistanceToActualBaseline(baseline); } @override void performLayout() { final BoxConstraints constraints = this.constraints; _placeholderDimensions = layoutInlineChildren( constraints.maxWidth, ChildLayoutHelper.layoutChild, ChildLayoutHelper.getBaseline, ); final (double minWidth, double maxWidth) = _adjustConstraints( minWidth: constraints.minWidth, maxWidth: constraints.maxWidth, ); _textPainter ..setPlaceholderDimensions(_placeholderDimensions) ..layout(minWidth: minWidth, maxWidth: maxWidth); positionInlineChildren(_textPainter.inlinePlaceholderBoxes!); _computeCaretPrototype(); final double width = forceLine ? constraints.maxWidth : constraints.constrainWidth(_textPainter.width + _caretMargin); assert(maxLines != 1 || _textPainter.maxLines == 1); final double preferredHeight = switch (maxLines) { null => math.max( _textPainter.height, preferredLineHeight * (minLines ?? 0), ), 1 => _textPainter.height, final int maxLines => clampDouble( _textPainter.height, preferredLineHeight * (minLines ?? maxLines), preferredLineHeight * maxLines, ), }; size = Size(width, constraints.constrainHeight(preferredHeight)); final contentSize = Size( _textPainter.width + _caretMargin, _textPainter.height, ); final painterConstraints = BoxConstraints.tight(contentSize); _foregroundRenderObject?.layout(painterConstraints); _backgroundRenderObject?.layout(painterConstraints); _maxScrollExtent = _getMaxScrollExtent(contentSize); offset ..applyViewportDimension(_viewportExtent) ..applyContentDimensions(0.0, _maxScrollExtent); } // The relative origin in relation to the distance the user has theoretically // dragged the floating cursor offscreen. This value is used to account for the // difference in the rendering position and the raw offset value. Offset _relativeOrigin = Offset.zero; Offset? _previousOffset; bool _shouldResetOrigin = true; bool _resetOriginOnLeft = false; bool _resetOriginOnRight = false; bool _resetOriginOnTop = false; bool _resetOriginOnBottom = false; double? _resetFloatingCursorAnimationValue; static Offset _calculateAdjustedCursorOffset( Offset offset, Rect boundingRects, ) { final double adjustedX = clampDouble( offset.dx, boundingRects.left, boundingRects.right, ); final double adjustedY = clampDouble( offset.dy, boundingRects.top, boundingRects.bottom, ); return Offset(adjustedX, adjustedY); } /// Returns the position within the text field closest to the raw cursor offset. /// /// See also: /// /// * [FloatingCursorDragState], which explains the floating cursor feature /// in detail. Offset calculateBoundedFloatingCursorOffset( Offset rawCursorOffset, { bool? shouldResetOrigin, }) { Offset deltaPosition = Offset.zero; final double topBound = -floatingCursorAddedMargin.top; final double bottomBound = math.min(size.height, _textPainter.height) - preferredLineHeight + floatingCursorAddedMargin.bottom; final double leftBound = -floatingCursorAddedMargin.left; final double rightBound = math.min(size.width, _textPainter.width) + floatingCursorAddedMargin.right; final boundingRects = Rect.fromLTRB( leftBound, topBound, rightBound, bottomBound, ); if (shouldResetOrigin != null) { _shouldResetOrigin = shouldResetOrigin; } if (!_shouldResetOrigin) { return _calculateAdjustedCursorOffset(rawCursorOffset, boundingRects); } if (_previousOffset != null) { deltaPosition = rawCursorOffset - _previousOffset!; } // If the raw cursor offset has gone off an edge, we want to reset the relative // origin of the dragging when the user drags back into the field. if (_resetOriginOnLeft && deltaPosition.dx > 0) { _relativeOrigin = Offset( rawCursorOffset.dx - boundingRects.left, _relativeOrigin.dy, ); _resetOriginOnLeft = false; } else if (_resetOriginOnRight && deltaPosition.dx < 0) { _relativeOrigin = Offset( rawCursorOffset.dx - boundingRects.right, _relativeOrigin.dy, ); _resetOriginOnRight = false; } if (_resetOriginOnTop && deltaPosition.dy > 0) { _relativeOrigin = Offset( _relativeOrigin.dx, rawCursorOffset.dy - boundingRects.top, ); _resetOriginOnTop = false; } else if (_resetOriginOnBottom && deltaPosition.dy < 0) { _relativeOrigin = Offset( _relativeOrigin.dx, rawCursorOffset.dy - boundingRects.bottom, ); _resetOriginOnBottom = false; } final double currentX = rawCursorOffset.dx - _relativeOrigin.dx; final double currentY = rawCursorOffset.dy - _relativeOrigin.dy; final Offset adjustedOffset = _calculateAdjustedCursorOffset( Offset(currentX, currentY), boundingRects, ); if (currentX < boundingRects.left && deltaPosition.dx < 0) { _resetOriginOnLeft = true; } else if (currentX > boundingRects.right && deltaPosition.dx > 0) { _resetOriginOnRight = true; } if (currentY < boundingRects.top && deltaPosition.dy < 0) { _resetOriginOnTop = true; } else if (currentY > boundingRects.bottom && deltaPosition.dy > 0) { _resetOriginOnBottom = true; } _previousOffset = rawCursorOffset; return adjustedOffset; } /// Sets the screen position of the floating cursor and the text position /// closest to the cursor. /// /// See also: /// /// * [FloatingCursorDragState], which explains the floating cursor feature /// in detail. void setFloatingCursor( FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue, }) { if (state == FloatingCursorDragState.End) { _relativeOrigin = Offset.zero; _previousOffset = null; _shouldResetOrigin = true; _resetOriginOnBottom = false; _resetOriginOnTop = false; _resetOriginOnRight = false; _resetOriginOnBottom = false; } _floatingCursorOn = state != FloatingCursorDragState.End; _resetFloatingCursorAnimationValue = resetLerpValue; if (_floatingCursorOn) { _floatingCursorTextPosition = lastTextPosition; final double? animationValue = _resetFloatingCursorAnimationValue; final EdgeInsets sizeAdjustment = animationValue != null ? EdgeInsets.lerp( _kFloatingCursorSizeIncrease, EdgeInsets.zero, animationValue, )! : _kFloatingCursorSizeIncrease; _caretPainter.floatingCursorRect = sizeAdjustment .inflateRect(_caretPrototype) .shift(boundedOffset); } else { _caretPainter.floatingCursorRect = null; } _caretPainter.showRegularCaret = _resetFloatingCursorAnimationValue == null; } MapEntry _lineNumberFor( TextPosition startPosition, List metrics, ) { // TODO(LongCatIsLooong): include line boundaries information in // ui.LineMetrics, then we can get rid of this. final Offset offset = _textPainter.getOffsetForCaret( startPosition, Rect.zero, ); for (final lineMetrics in metrics) { if (lineMetrics.baseline > offset.dy) { return MapEntry( lineMetrics.lineNumber, Offset(offset.dx, lineMetrics.baseline), ); } } assert( startPosition.offset == 0, 'unable to find the line for $startPosition', ); return MapEntry( math.max(0, metrics.length - 1), Offset( offset.dx, metrics.isNotEmpty ? metrics.last.baseline + metrics.last.descent : 0.0, ), ); } /// Starts a [VerticalCaretMovementRun] at the given location in the text, for /// handling consecutive vertical caret movements. /// /// This can be used to handle consecutive upward/downward arrow key movements /// in an input field. /// /// {@macro flutter.rendering.RenderEditable.verticalArrowKeyMovement} /// /// The [VerticalCaretMovementRun.isValid] property indicates whether the text /// layout has changed and the vertical caret run is invalidated. /// /// The caller should typically discard a [VerticalCaretMovementRun] when /// its [VerticalCaretMovementRun.isValid] becomes false, or on other /// occasions where the vertical caret run should be interrupted. VerticalCaretMovementRun startVerticalCaretMovement( TextPosition startPosition, ) { final List metrics = _textPainter.computeLineMetrics(); final MapEntry currentLine = _lineNumberFor( startPosition, metrics, ); return VerticalCaretMovementRun._( this, metrics, startPosition, currentLine.key, currentLine.value, ); } void _paintContents(PaintingContext context, Offset offset) { final Offset effectiveOffset = offset + _paintOffset; if (selection != null && !_floatingCursorOn) { _updateSelectionExtentsVisibility(effectiveOffset); } final RenderBox? foregroundChild = _foregroundRenderObject; final RenderBox? backgroundChild = _backgroundRenderObject; // The painters paint in the viewport's coordinate space, since the // textPainter's coordinate space is not known to high level widgets. if (backgroundChild != null) { context.paintChild(backgroundChild, offset); } _textPainter.paint(context.canvas, effectiveOffset); paintInlineChildren(context, effectiveOffset); if (foregroundChild != null) { context.paintChild(foregroundChild, offset); } } final LayerHandle _leaderLayerHandler = LayerHandle(); void _paintHandleLayers( PaintingContext context, List endpoints, Offset offset, ) { Offset startPoint = endpoints[0].point; startPoint = Offset( clampDouble(startPoint.dx, 0.0, size.width), clampDouble(startPoint.dy, 0.0, size.height), ); _leaderLayerHandler.layer = LeaderLayer( link: startHandleLayerLink, offset: startPoint + offset, ); context.pushLayer(_leaderLayerHandler.layer!, super.paint, Offset.zero); if (endpoints.length == 2) { Offset endPoint = endpoints[1].point; endPoint = Offset( clampDouble(endPoint.dx, 0.0, size.width), clampDouble(endPoint.dy, 0.0, size.height), ); context.pushLayer( LeaderLayer(link: endHandleLayerLink, offset: endPoint + offset), super.paint, Offset.zero, ); } else if (selection!.isCollapsed) { context.pushLayer( LeaderLayer(link: endHandleLayerLink, offset: startPoint + offset), super.paint, Offset.zero, ); } } @override void applyPaintTransform(RenderBox child, Matrix4 transform) { if (child == _foregroundRenderObject || child == _backgroundRenderObject) { return; } defaultApplyPaintTransform(child, transform); } @override void paint(PaintingContext context, Offset offset) { _computeTextMetricsIfNeeded(); if (_hasVisualOverflow && clipBehavior != Clip.none) { _clipRectLayer.layer = context.pushClipRect( needsCompositing, offset, Offset.zero & size, _paintContents, clipBehavior: clipBehavior, oldLayer: _clipRectLayer.layer, ); } else { _clipRectLayer.layer = null; _paintContents(context, offset); } final TextSelection? selection = this.selection; if (selection != null && selection.isValid) { _paintHandleLayers(context, getEndpointsForSelection(selection), offset); } } final LayerHandle _clipRectLayer = LayerHandle(); @override Rect? describeApproximatePaintClip(RenderObject child) { switch (clipBehavior) { case Clip.none: return null; case Clip.hardEdge: case Clip.antiAlias: case Clip.antiAliasWithSaveLayer: return _hasVisualOverflow ? Offset.zero & size : null; } } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(ColorProperty('cursorColor', cursorColor)) ..add( DiagnosticsProperty>('showCursor', showCursor), ) ..add(IntProperty('maxLines', maxLines)) ..add(IntProperty('minLines', minLines)) ..add( DiagnosticsProperty('expands', expands, defaultValue: false), ) ..add(ColorProperty('selectionColor', selectionColor)) ..add( DiagnosticsProperty( 'textScaler', textScaler, defaultValue: TextScaler.noScaling, ), ) ..add( DiagnosticsProperty('locale', locale, defaultValue: null), ) ..add(DiagnosticsProperty('selection', selection)) ..add(DiagnosticsProperty('offset', offset)); } @override List debugDescribeChildren() { return [ if (text != null) text!.toDiagnosticsNode( name: 'text', style: DiagnosticsTreeStyle.transition, ), ]; } } class _RenderEditableCustomPaint extends RenderBox { _RenderEditableCustomPaint({RenderEditablePainter? painter}) : _painter = painter, super(); @override RenderEditable? get parent => super.parent as RenderEditable?; @override bool get isRepaintBoundary => true; @override bool get sizedByParent => true; RenderEditablePainter? get painter => _painter; RenderEditablePainter? _painter; set painter(RenderEditablePainter? newValue) { if (newValue == painter) { return; } final RenderEditablePainter? oldPainter = painter; _painter = newValue; if (newValue?.shouldRepaint(oldPainter) ?? true) { markNeedsPaint(); } if (attached) { oldPainter?.removeListener(markNeedsPaint); newValue?.addListener(markNeedsPaint); } } @override void paint(PaintingContext context, Offset offset) { final RenderEditable? parent = this.parent; assert(parent != null); final RenderEditablePainter? painter = this.painter; if (painter != null && parent != null) { parent._computeTextMetricsIfNeeded(); painter.paint(context.canvas, size, parent); } } @override void attach(PipelineOwner owner) { super.attach(owner); _painter?.addListener(markNeedsPaint); } @override void detach() { _painter?.removeListener(markNeedsPaint); super.detach(); } @override @protected Size computeDryLayout(covariant BoxConstraints constraints) => constraints.biggest; } /// An interface that paints within a [RenderEditable]'s bounds, above or /// beneath its text content. /// /// This painter is typically used for painting auxiliary content that depends /// on text layout metrics (for instance, for painting carets and text highlight /// blocks). It can paint independently from its [RenderEditable], allowing it /// to repaint without triggering a repaint on the entire [RenderEditable] stack /// when only auxiliary content changes (e.g. a blinking cursor) are present. It /// will be scheduled to repaint when: /// /// * It's assigned to a new [RenderEditable] (replacing a prior /// [RenderEditablePainter]) and the [shouldRepaint] method returns true. /// * Any of the [RenderEditable]s it is attached to repaints. /// * The [notifyListeners] method is called, which typically happens when the /// painter's attributes change. /// /// See also: /// /// * [RenderEditable.foregroundPainter], which takes a [RenderEditablePainter] /// and sets it as the foreground painter of the [RenderEditable]. /// * [RenderEditable.painter], which takes a [RenderEditablePainter] /// and sets it as the background painter of the [RenderEditable]. /// * [CustomPainter], a similar class which paints within a [RenderCustomPaint]. abstract class RenderEditablePainter extends ChangeNotifier { /// Determines whether repaint is needed when a new [RenderEditablePainter] /// is provided to a [RenderEditable]. /// /// If the new instance represents different information than the old /// instance, then the method should return true, otherwise it should return /// false. When [oldDelegate] is null, this method should always return true /// unless the new painter initially does not paint anything. /// /// If the method returns false, then the [paint] call might be optimized /// away. However, the [paint] method will get called whenever the /// [RenderEditable]s it attaches to repaint, even if [shouldRepaint] returns /// false. bool shouldRepaint(RenderEditablePainter? oldDelegate); /// Paints within the bounds of a [RenderEditable]. /// /// The given [Canvas] has the same coordinate space as the [RenderEditable], /// which may be different from the coordinate space the [RenderEditable]'s /// [TextPainter] uses, when the text moves inside the [RenderEditable]. /// /// Paint operations performed outside of the region defined by the [canvas]'s /// origin and the [size] parameter may get clipped, when [RenderEditable]'s /// [RenderEditable.clipBehavior] is not [Clip.none]. void paint(Canvas canvas, Size size, RenderEditable renderEditable); } class _TextHighlightPainter extends RenderEditablePainter { _TextHighlightPainter({TextRange? highlightedRange, Color? highlightColor}) : _highlightedRange = highlightedRange, _highlightColor = highlightColor; final Paint highlightPaint = Paint(); Color? get highlightColor => _highlightColor; Color? _highlightColor; set highlightColor(Color? newValue) { if (newValue == _highlightColor) { return; } _highlightColor = newValue; notifyListeners(); } TextRange? get highlightedRange => _highlightedRange; TextRange? _highlightedRange; set highlightedRange(TextRange? newValue) { if (newValue == _highlightedRange) { return; } _highlightedRange = newValue; notifyListeners(); } /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. ui.BoxHeightStyle get selectionHeightStyle => _selectionHeightStyle; ui.BoxHeightStyle _selectionHeightStyle = ui.BoxHeightStyle.tight; set selectionHeightStyle(ui.BoxHeightStyle value) { if (_selectionHeightStyle == value) { return; } _selectionHeightStyle = value; notifyListeners(); } /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. ui.BoxWidthStyle get selectionWidthStyle => _selectionWidthStyle; ui.BoxWidthStyle _selectionWidthStyle = ui.BoxWidthStyle.tight; set selectionWidthStyle(ui.BoxWidthStyle value) { if (_selectionWidthStyle == value) { return; } _selectionWidthStyle = value; notifyListeners(); } @override void paint(Canvas canvas, Size size, RenderEditable renderEditable) { final TextRange? range = highlightedRange; final Color? color = highlightColor; if (range == null || color == null || range.isCollapsed) { return; } highlightPaint.color = color; final TextPainter textPainter = renderEditable._textPainter; final Set boxes = textPainter .getBoxesForSelection( TextSelection(baseOffset: range.start, extentOffset: range.end), boxHeightStyle: selectionHeightStyle, boxWidthStyle: selectionWidthStyle, ) .toSet(); for (final box in boxes) { canvas.drawRect( box .toRect() .shift(renderEditable._paintOffset) .intersect( Rect.fromLTRB(0, 0, textPainter.width, textPainter.height), ), highlightPaint, ); } } @override bool shouldRepaint(RenderEditablePainter? oldDelegate) { if (identical(oldDelegate, this)) { return false; } if (oldDelegate == null) { return highlightColor != null && highlightedRange != null; } return oldDelegate is! _TextHighlightPainter || oldDelegate.highlightColor != highlightColor || oldDelegate.highlightedRange != highlightedRange || oldDelegate.selectionHeightStyle != selectionHeightStyle || oldDelegate.selectionWidthStyle != selectionWidthStyle; } } class _CaretPainter extends RenderEditablePainter { _CaretPainter(); bool get shouldPaint => _shouldPaint; bool _shouldPaint = true; set shouldPaint(bool value) { if (shouldPaint == value) { return; } _shouldPaint = value; notifyListeners(); } // This is directly manipulated by the RenderEditable during // setFloatingCursor. // // When changing this value, the caller is responsible for ensuring that // listeners are notified. bool showRegularCaret = false; final Paint caretPaint = Paint(); late final Paint floatingCursorPaint = Paint(); Color? get caretColor => _caretColor; Color? _caretColor; set caretColor(Color? value) { if (caretColor?.toARGB32() == value?.toARGB32()) { return; } _caretColor = value; notifyListeners(); } Radius? get cursorRadius => _cursorRadius; Radius? _cursorRadius; set cursorRadius(Radius? value) { if (_cursorRadius == value) { return; } _cursorRadius = value; notifyListeners(); } Offset get cursorOffset => _cursorOffset; Offset _cursorOffset = Offset.zero; set cursorOffset(Offset value) { if (_cursorOffset == value) { return; } _cursorOffset = value; notifyListeners(); } Color? get backgroundCursorColor => _backgroundCursorColor; Color? _backgroundCursorColor; set backgroundCursorColor(Color? value) { if (backgroundCursorColor?.toARGB32() == value?.toARGB32()) { return; } _backgroundCursorColor = value; if (showRegularCaret) { notifyListeners(); } } Rect? get floatingCursorRect => _floatingCursorRect; Rect? _floatingCursorRect; set floatingCursorRect(Rect? value) { if (_floatingCursorRect == value) { return; } _floatingCursorRect = value; notifyListeners(); } void paintRegularCursor( Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition, ) { final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition); if (shouldPaint) { if (floatingCursorRect != null) { final double distanceSquared = (floatingCursorRect!.center - integralRect.center).distanceSquared; if (distanceSquared < _kShortestDistanceSquaredWithFloatingAndRegularCursors) { return; } } final Radius? radius = cursorRadius; caretPaint.color = caretColor; if (radius == null) { canvas.drawRect(integralRect, caretPaint); } else { final caretRRect = RRect.fromRectAndRadius(integralRect, radius); canvas.drawRRect(caretRRect, caretPaint); } } } @override void paint(Canvas canvas, Size size, RenderEditable renderEditable) { // Compute the caret location even when `shouldPaint` is false. final TextSelection? selection = renderEditable.selection; if (selection == null || !selection.isCollapsed || !selection.isValid) { return; } final Rect? floatingCursorRect = this.floatingCursorRect; final Color? caretColor = floatingCursorRect == null ? this.caretColor : showRegularCaret ? backgroundCursorColor : null; final TextPosition caretTextPosition = floatingCursorRect == null ? selection.extent : renderEditable._floatingCursorTextPosition; if (caretColor != null) { paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition); } final Color? floatingCursorColor = this.caretColor?.withValues(alpha: 0.75); // Floating Cursor. if (floatingCursorRect == null || floatingCursorColor == null || !shouldPaint) { return; } canvas.drawRRect( RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCursorRadius), floatingCursorPaint..color = floatingCursorColor, ); } @override bool shouldRepaint(RenderEditablePainter? oldDelegate) { if (identical(this, oldDelegate)) { return false; } if (oldDelegate == null) { return shouldPaint; } return oldDelegate is! _CaretPainter || oldDelegate.shouldPaint != shouldPaint || oldDelegate.showRegularCaret != showRegularCaret || oldDelegate.caretColor != caretColor || oldDelegate.cursorRadius != cursorRadius || oldDelegate.cursorOffset != cursorOffset || oldDelegate.backgroundCursorColor != backgroundCursorColor || oldDelegate.floatingCursorRect != floatingCursorRect; } } class _CompositeRenderEditablePainter extends RenderEditablePainter { _CompositeRenderEditablePainter({required this.painters}); final List painters; @override void addListener(VoidCallback listener) { for (final RenderEditablePainter painter in painters) { painter.addListener(listener); } } @override void removeListener(VoidCallback listener) { for (final RenderEditablePainter painter in painters) { painter.removeListener(listener); } } @override void paint(Canvas canvas, Size size, RenderEditable renderEditable) { for (final RenderEditablePainter painter in painters) { painter.paint(canvas, size, renderEditable); } } @override bool shouldRepaint(RenderEditablePainter? oldDelegate) { if (identical(oldDelegate, this)) { return false; } if (oldDelegate is! _CompositeRenderEditablePainter || oldDelegate.painters.length != painters.length) { return true; } final Iterator oldPainters = oldDelegate.painters.iterator; final Iterator newPainters = painters.iterator; while (oldPainters.moveNext() && newPainters.moveNext()) { if (newPainters.current.shouldRepaint(oldPainters.current)) { return true; } } return false; } } ================================================ FILE: lib/common/widgets/flutter/text_field/editable_text.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'package:flutter/cupertino.dart'; /// @docImport 'package:flutter/material.dart'; /// /// @docImport 'app.dart'; /// @docImport 'context_menu_controller.dart'; /// @docImport 'form.dart'; /// @docImport 'restoration.dart'; /// @docImport 'restoration_properties.dart'; /// @docImport 'selectable_region.dart'; /// @docImport 'text_selection_toolbar_layout_delegate.dart'; library; import 'dart:async'; import 'dart:io' show Platform; import 'dart:math' as math; import 'dart:ui' as ui hide TextStyle; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide EditableText, EditableTextState, SpellCheckConfiguration, TextSelectionGestureDetectorBuilder, TextSelectionOverlay; import 'package:flutter/rendering.dart' hide RenderEditable, VerticalCaretMovementRun; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; /// Signature for a widget builder that builds a context menu for the given /// [EditableTextState]. /// /// See also: /// /// * [SelectableRegionContextMenuBuilder], which performs the same role for /// [SelectableRegion]. typedef EditableTextContextMenuBuilder = Widget Function(BuildContext context, EditableTextState editableTextState); // Signature for a function that determines the target location of the given // [TextPosition] after applying the given [TextBoundary]. typedef _ApplyTextBoundary = TextPosition Function(TextPosition, bool, TextBoundary); // The time it takes for the cursor to fade from fully opaque to fully // transparent and vice versa. A full cursor blink, from transparent to opaque // to transparent, is twice this duration. const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500); // Number of cursor ticks during which the most recently entered character // is shown in an obscured text field. const int _kObscureShowLatestCharCursorTicks = 3; class _CompositionCallback extends SingleChildRenderObjectWidget { const _CompositionCallback({ required this.compositeCallback, required this.enabled, super.child, }); final CompositionCallback compositeCallback; final bool enabled; @override RenderObject createRenderObject(BuildContext context) { return _RenderCompositionCallback(compositeCallback, enabled); } @override void updateRenderObject( BuildContext context, _RenderCompositionCallback renderObject, ) { super.updateRenderObject(context, renderObject); // _EditableTextState always uses the same callback. assert(renderObject.compositeCallback == compositeCallback); renderObject.enabled = enabled; } } class _RenderCompositionCallback extends RenderProxyBox { _RenderCompositionCallback(this.compositeCallback, this._enabled); final CompositionCallback compositeCallback; VoidCallback? _cancelCallback; bool get enabled => _enabled; bool _enabled = false; set enabled(bool newValue) { _enabled = newValue; if (!newValue) { _cancelCallback?.call(); _cancelCallback = null; } else if (_cancelCallback == null) { markNeedsPaint(); } } @override void paint(PaintingContext context, ui.Offset offset) { if (enabled) { _cancelCallback ??= context.addCompositionCallback(compositeCallback); } super.paint(context, offset); } } // A time-value pair that represents a key frame in an animation. class _KeyFrame { const _KeyFrame(this.time, this.value); // Values extracted from iOS 15.4 UIKit. static const List<_KeyFrame> iOSBlinkingCaretKeyFrames = <_KeyFrame>[ _KeyFrame(0, 1), // 0 _KeyFrame(0.5, 1), // 1 _KeyFrame(0.5375, 0.75), // 2 _KeyFrame(0.575, 0.5), // 3 _KeyFrame(0.6125, 0.25), // 4 _KeyFrame(0.65, 0), // 5 _KeyFrame(0.85, 0), // 6 _KeyFrame(0.8875, 0.25), // 7 _KeyFrame(0.925, 0.5), // 8 _KeyFrame(0.9625, 0.75), // 9 _KeyFrame(1, 1), // 10 ]; // The timing, in seconds, of the specified animation `value`. final double time; final double value; } class _DiscreteKeyFrameSimulation extends Simulation { _DiscreteKeyFrameSimulation.iOSBlinkingCaret() : this._(_KeyFrame.iOSBlinkingCaretKeyFrames, 1); _DiscreteKeyFrameSimulation._(this._keyFrames, this.maxDuration) : assert(_keyFrames.isNotEmpty), assert(_keyFrames.last.time <= maxDuration), assert(() { for (var i = 0; i < _keyFrames.length - 1; i += 1) { if (_keyFrames[i].time > _keyFrames[i + 1].time) { return false; } } return true; }(), 'The key frame sequence must be sorted by time.'); final double maxDuration; final List<_KeyFrame> _keyFrames; @override double dx(double time) => 0; @override bool isDone(double time) => time >= maxDuration; // The index of the KeyFrame corresponds to the most recent input `time`. int _lastKeyFrameIndex = 0; @override double x(double time) { final int length = _keyFrames.length; // Perform a linear search in the sorted key frame list, starting from the // last key frame found, since the input `time` usually monotonically // increases by a small amount. int searchIndex; final int endIndex; if (_keyFrames[_lastKeyFrameIndex].time > time) { // The simulation may have restarted. Search within the index range // [0, _lastKeyFrameIndex). searchIndex = 0; endIndex = _lastKeyFrameIndex; } else { searchIndex = _lastKeyFrameIndex; endIndex = length; } // Find the target key frame. Don't have to check (endIndex - 1): if // (endIndex - 2) doesn't work we'll have to pick (endIndex - 1) anyways. while (searchIndex < endIndex - 1) { assert(_keyFrames[searchIndex].time <= time); final _KeyFrame next = _keyFrames[searchIndex + 1]; if (time < next.time) { break; } searchIndex += 1; } _lastKeyFrameIndex = searchIndex; return _keyFrames[_lastKeyFrameIndex].value; } } /// A basic text input field. /// /// This widget interacts with the [TextInput] service to let the user edit the /// text it contains. It also provides scrolling, selection, and cursor /// movement. /// /// The [EditableText] widget is a low-level widget that is intended as a /// building block for custom widget sets. For a complete user experience, /// consider using a [TextField] or [CupertinoTextField]. /// /// ## Handling User Input /// /// Currently the user may change the text this widget contains via keyboard or /// the text selection menu. When the user inserted or deleted text, you will be /// notified of the change and get a chance to modify the new text value: /// /// * The [inputFormatters] will be first applied to the user input. /// /// * The [controller]'s [RichTextEditingController.value] will be updated with the /// formatted result, and the [controller]'s listeners will be notified. /// /// * The [onChanged] callback, if specified, will be called last. /// /// ## Input Actions /// /// A [TextInputAction] can be provided to customize the appearance of the /// action button on the soft keyboard for Android and iOS. The default action /// is [TextInputAction.done]. /// /// Many [TextInputAction]s are common between Android and iOS. However, if a /// [textInputAction] is provided that is not supported by the current /// platform in debug mode, an error will be thrown when the corresponding /// EditableText receives focus. For example, providing iOS's "emergencyCall" /// action when running on an Android device will result in an error when in /// debug mode. In release mode, incompatible [TextInputAction]s are replaced /// either with "unspecified" on Android, or "default" on iOS. Appropriate /// [textInputAction]s can be chosen by checking the current platform and then /// selecting the appropriate action. /// /// {@template flutter.widgets.EditableText.lifeCycle} /// ## Lifecycle /// /// Upon completion of editing, like pressing the "done" button on the keyboard, /// two actions take place: /// /// 1st: Editing is finalized. The default behavior of this step includes /// an invocation of [onChanged]. That default behavior can be overridden. /// See [onEditingComplete] for details. /// /// 2nd: [onSubmitted] is invoked with the user's input value. /// /// [onSubmitted] can be used to manually move focus to another input widget /// when a user finishes with the currently focused input widget. /// /// When the widget has focus, it will prevent itself from disposing via /// [AutomaticKeepAliveClientMixin.wantKeepAlive] in order to avoid losing the /// selection. Removing the focus will allow it to be disposed. /// {@endtemplate} /// /// Rather than using this widget directly, consider using [TextField], which /// is a full-featured, material-design text input field with placeholder text, /// labels, and [Form] integration. /// /// ## Text Editing [Intent]s and Their Default [Action]s /// /// This widget provides default [Action]s for handling common text editing /// [Intent]s such as deleting, copying and pasting in the text field. These /// [Action]s can be directly invoked using [Actions.invoke] or the /// [Actions.maybeInvoke] method. The default text editing keyboard [Shortcuts], /// typically declared in [DefaultTextEditingShortcuts], also use these /// [Intent]s and [Action]s to perform the text editing operations they are /// bound to. /// /// The default handling of a specific [Intent] can be overridden by placing an /// [Actions] widget above this widget. See the [Action] class and the /// [Action.overridable] constructor for more information on how a pre-defined /// overridable [Action] can be overridden. /// /// ### Intents for Deleting Text and Their Default Behavior /// /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a [caret](https://en.wikipedia.org/wiki/Caret_navigation) (The selection is [TextSelection.collapsed])** | /// | :------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------- | /// | [DeleteCharacterIntent] | Deletes the selected text | Deletes the user-perceived character before or after the caret location. | /// | [DeleteToNextWordBoundaryIntent] | Deletes the selected text and the word before/after the selection's [TextSelection.extent] position | Deletes from the caret location to the previous or the next word boundary | /// | [DeleteToLineBreakIntent] | Deletes the selected text, and deletes to the start/end of the line from the selection's [TextSelection.extent] position | Deletes from the caret location to the logical start or end of the current line | /// /// ### Intents for Moving the [Caret](https://en.wikipedia.org/wiki/Caret_navigation) /// /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** | /// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- | /// | [ExtendSelectionByCharacterIntent](`collapseSelection: true`) | Collapses the selection to the logical start/end of the selection | Moves the caret past the user-perceived character before or after the current caret location. | /// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position | Moves the caret to the previous/next word boundary. | /// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position, or [TextSelection.base], whichever is closest in the given direction | Moves the caret to the previous/next word boundary. | /// | [ExtendSelectionToLineBreakIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the line at the selection's [TextSelection.extent] position | Moves the caret to the start/end of the current line .| /// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent line | Moves the caret to the closest position on the previous/next adjacent line. | /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent page | Moves the caret to the closest position on the previous/next adjacent page. | /// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. | /// /// #### Intents for Extending the Selection /// /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** | /// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- | /// | [ExtendSelectionByCharacterIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] past the user-perceived character before/after it | /// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary | /// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary, or [TextSelection.base] whichever is closest in the given direction | Moves the selection's [TextSelection.extent] to the previous/next word boundary. | /// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line | /// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line | /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent page | /// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document | /// | [SelectAllTextIntent] | Selects the entire document | /// /// ### Other Intents /// /// | **Intent Class** | **Default Behavior** | /// | :-------------------------------------- | :--------------------------------------------------- | /// | [DoNothingAndStopPropagationTextIntent] | Does nothing in the input field, and prevents the key event from further propagating in the widget tree. | /// | [ReplaceTextIntent] | Replaces the current [TextEditingValue] in the input field's [RichTextEditingController], and triggers all related user callbacks and [TextInputFormatter]s. | /// | [UpdateSelectionIntent] | Updates the current selection in the input field's [RichTextEditingController], and triggers the [onSelectionChanged] callback. | /// | [CopySelectionTextIntent] | Copies or cuts the selected text into the clipboard | /// | [PasteTextIntent] | Inserts the current text in the clipboard after the caret location, or replaces the selected text if the selection is not collapsed. | /// /// ## Text Editing [Shortcuts] /// /// It's also possible to directly remap keyboard shortcuts to new [Intent]s by /// inserting a [Shortcuts] widget above this in the widget tree. When using /// [WidgetsApp], the large set of default text editing keyboard shortcuts are /// declared near the top of the widget tree in [DefaultTextEditingShortcuts], /// and any [Shortcuts] widget between it and this [EditableText] will override /// those defaults. /// /// {@template flutter.widgets.editableText.shortcutsAndTextInput} /// ### Interactions Between [Shortcuts] and Text Input /// /// Shortcuts prevent text input fields from receiving their keystrokes as text /// input. For example, placing a [Shortcuts] widget in the widget tree above /// a text input field and creating a shortcut for [LogicalKeyboardKey.keyA] /// will prevent the field from receiving that key as text input. In other /// words, typing key "A" into the field will trigger the shortcut and will not /// insert a letter "a" into the field. /// /// This happens because of the way that key strokes are handled in Flutter. /// When a keystroke is received in Flutter's engine, it first gives the /// framework the opportunity to handle it as a raw key event through /// [SystemChannels.keyEvent]. This is what [Shortcuts] listens to indirectly /// through its [FocusNode]. If it is not handled, then it will proceed to try /// handling it as text input through [SystemChannels.textInput], which is what /// [EditableTextState] listens to through [TextInputClient]. /// /// This behavior, where a shortcut prevents text input into some field, can be /// overridden by using another [Shortcuts] widget lower in the widget tree and /// mapping the desired key stroke(s) to [DoNothingAndStopPropagationIntent]. /// The key event will be reported as unhandled by the framework and will then /// be sent as text input as usual. /// {@endtemplate} /// /// ## Gesture Events Handling /// /// When [rendererIgnoresPointer] is false (the default), this widget provides /// rudimentary, platform-agnostic gesture handling for user actions such as /// tapping, long-pressing, and scrolling. /// /// To provide more complete gesture handling, including double-click to select /// a word, drag selection, and platform-specific handling of gestures such as /// long presses, consider setting [rendererIgnoresPointer] to true and using /// [TextSelectionGestureDetectorBuilder]. /// /// {@template flutter.widgets.editableText.showCaretOnScreen} /// ## Keep the caret visible when focused /// /// When focused, this widget will make attempts to keep the text area and its /// caret (even when [showCursor] is `false`) visible, on these occasions: /// /// * When the user focuses this text field and it is not [readOnly]. /// * When the user changes the selection of the text field, or changes the /// text when the text field is not [readOnly]. /// * When the virtual keyboard pops up. /// {@endtemplate} /// /// ## Scrolling Considerations /// /// If this [EditableText] is not a descendant of [Scaffold] and is being used /// within a [Scrollable] or nested [Scrollable]s, consider placing a /// [ScrollNotificationObserver] above the root [Scrollable] that contains this /// [EditableText] to ensure proper scroll coordination for [EditableText] and /// its components like [TextSelectionOverlay]. /// /// {@template flutter.widgets.editableText.accessibility} /// ## Troubleshooting Common Accessibility Issues /// /// ### Customizing User Input Accessibility Announcements /// /// To customize user input accessibility announcements triggered by text /// changes, use [SemanticsService.announce] to make the desired /// accessibility announcement. /// /// On iOS, the on-screen keyboard may announce the most recent input /// incorrectly when a [TextInputFormatter] inserts a thousands separator to /// a currency value text field. The following example demonstrates how to /// suppress the default accessibility announcements by always announcing /// the content of the text field as a US currency value (the `\$` inserts /// a dollar sign, the `$newText` interpolates the `newText` variable): /// /// ```dart /// onChanged: (String newText) { /// if (newText.isNotEmpty) { /// SemanticsService.sendAnnouncement( /// View.of(context), /// '\$$newText', /// Directionality.of(context), /// ); /// } /// } /// ``` /// /// {@endtemplate} /// /// See also: /// /// * [TextField], which is a full-featured, material-design text input field /// with placeholder text, labels, and [Form] integration. class EditableText extends StatefulWidget { /// Creates a basic text input control. /// /// The [maxLines] property can be set to null to remove the restriction on /// the number of lines. By default, it is one, meaning this is a single-line /// text field. [maxLines] must be null or greater than zero. /// /// If [keyboardType] is not set or is null, its value will be inferred from /// [autofillHints], if [autofillHints] is not empty. Otherwise it defaults to /// [TextInputType.text] if [maxLines] is exactly one, and /// [TextInputType.multiline] if [maxLines] is null or greater than one. /// /// The text cursor is not shown if [showCursor] is false or if [showCursor] /// is null (the default) and [readOnly] is true. EditableText({ super.key, required this.controller, required this.focusNode, this.readOnly = false, this.obscuringCharacter = '•', this.obscureText = false, bool? autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, required this.style, StrutStyle? strutStyle, required this.cursorColor, required this.backgroundCursorColor, this.textAlign = TextAlign.start, this.textDirection, this.locale, @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) this.textScaleFactor, this.textScaler, this.maxLines = 1, this.minLines, this.expands = false, this.forceLine = true, this.textHeightBehavior, this.textWidthBasis = TextWidthBasis.parent, this.autofocus = false, bool? showCursor, this.showSelectionHandles = false, this.selectionColor, this.selectionControls, TextInputType? keyboardType, this.textInputAction, this.textCapitalization = TextCapitalization.none, this.onChanged, this.onEditingComplete, this.onSubmitted, this.onAppPrivateCommand, this.onSelectionChanged, this.onSelectionHandleTapped, this.groupId = EditableText, this.onTapOutside, this.onTapUpOutside, List? inputFormatters, this.mouseCursor, this.rendererIgnoresPointer = false, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, this.cursorOpacityAnimates = false, this.cursorOffset, this.paintCursorAboveText = false, ui.BoxHeightStyle? selectionHeightStyle, ui.BoxWidthStyle? selectionWidthStyle, this.scrollPadding = const EdgeInsets.all(20.0), this.keyboardAppearance = Brightness.light, this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, bool? selectAllOnFocus, this.scrollController, this.scrollPhysics, this.autocorrectionTextRectColor, @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) ToolbarOptions? toolbarOptions, this.autofillHints = const [], this.autofillClient, this.clipBehavior = Clip.hardEdge, this.restorationId, this.scrollBehavior, @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) this.scribbleEnabled = true, this.stylusHandwritingEnabled = defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, this.contentInsertionConfiguration, this.contextMenuBuilder, this.spellCheckConfiguration, this.magnifierConfiguration = TextMagnifierConfiguration.disabled, this.hintLocales, }) : assert(obscuringCharacter.length == 1), autocorrect = autocorrect ?? _inferAutocorrect(autofillHints: autofillHints), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( !expands || (maxLines == null && minLines == null), 'minLines and maxLines must be null when expands is true.', ), assert( !obscureText || maxLines == 1, 'Obscured fields cannot be multiline.', ), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText), selectAllOnFocus = selectAllOnFocus ?? _defaultSelectAllOnFocus, toolbarOptions = selectionControls is TextSelectionHandleControls && toolbarOptions == null ? ToolbarOptions.empty : toolbarOptions ?? (obscureText ? (readOnly // No point in even offering "Select All" in a read-only obscured // field. ? ToolbarOptions.empty // Writable, but obscured. : const ToolbarOptions(selectAll: true, paste: true)) : (readOnly // Read-only, not obscured. ? const ToolbarOptions(selectAll: true, copy: true) // Writable, not obscured. : const ToolbarOptions( copy: true, cut: true, selectAll: true, paste: true, ))), assert( spellCheckConfiguration == null || spellCheckConfiguration == const SpellCheckConfiguration.disabled() || spellCheckConfiguration.misspelledTextStyle != null, 'spellCheckConfiguration must specify a misspelledTextStyle if spell check behavior is desired', ), _strutStyle = strutStyle, keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines), inputFormatters = maxLines == 1 ? [ FilteringTextInputFormatter.singleLineFormatter, ...inputFormatters ?? const Iterable.empty(), ] : inputFormatters, showCursor = showCursor ?? !readOnly, selectionHeightStyle = selectionHeightStyle ?? defaultSelectionHeightStyle, selectionWidthStyle = selectionWidthStyle ?? defaultSelectionWidthStyle; /// Controls the text being edited. final RichTextEditingController controller; /// Controls whether this widget has keyboard focus. final FocusNode focusNode; /// {@template flutter.widgets.editableText.obscuringCharacter} /// Character used for obscuring text if [obscureText] is true. /// /// Must be only a single character. /// /// Defaults to the character U+2022 BULLET (•). /// {@endtemplate} final String obscuringCharacter; /// {@template flutter.widgets.editableText.obscureText} /// Whether to hide the text being edited (e.g., for passwords). /// /// When this is set to true, all the characters in the text field are /// replaced by [obscuringCharacter], and the text in the field cannot be /// copied with copy or cut. If [readOnly] is also true, then the text cannot /// be selected. /// /// Defaults to false. /// {@endtemplate} final bool obscureText; /// {@macro dart.ui.textHeightBehavior} final TextHeightBehavior? textHeightBehavior; /// {@macro flutter.painting.textPainter.textWidthBasis} final TextWidthBasis textWidthBasis; /// {@template flutter.widgets.editableText.readOnly} /// Whether the text can be changed. /// /// When this is set to true, the text cannot be modified /// by any shortcut or keyboard operation. The text is still selectable. /// /// Defaults to false. /// {@endtemplate} final bool readOnly; /// Whether the text will take the full width regardless of the text width. /// /// When this is set to false, the width will be based on text width, which /// will also be affected by [textWidthBasis]. /// /// Defaults to true. /// /// See also: /// /// * [textWidthBasis], which controls the calculation of text width. final bool forceLine; /// Configuration of toolbar options. /// /// By default, all options are enabled. If [readOnly] is true, paste and cut /// will be disabled regardless. If [obscureText] is true, cut and copy will /// be disabled regardless. If [readOnly] and [obscureText] are both true, /// select all will also be disabled. final ToolbarOptions toolbarOptions; /// Whether to show selection handles. /// /// When a selection is active, there will be two handles at each side of /// boundary, or one handle if the selection is collapsed. The handles can be /// dragged to adjust the selection. /// /// See also: /// /// * [showCursor], which controls the visibility of the cursor. final bool showSelectionHandles; /// {@template flutter.widgets.editableText.showCursor} /// Whether to show cursor. /// /// The cursor refers to the blinking caret when the [EditableText] is focused. /// {@endtemplate} /// /// See also: /// /// * [showSelectionHandles], which controls the visibility of the selection handles. final bool showCursor; /// {@template flutter.widgets.editableText.autocorrect} /// Whether to enable autocorrection. /// /// False on iOS if [autofillHints] contains password-related hints, otherwise true. /// {@endtemplate} final bool autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} final SmartQuotesType smartQuotesType; /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} final bool enableSuggestions; /// The text style to use for the editable text. /// /// The user or platform may override this [style]'s [TextStyle.fontWeight], /// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing] /// via a [MediaQuery] ancestor's [MediaQueryData.boldText], /// [MediaQueryData.lineHeightScaleFactorOverride], /// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride] /// regardless of its [TextStyle.inherit] value. final TextStyle style; /// {@template flutter.widgets.editableText.strutStyle} /// The strut style used for the vertical layout. /// /// [StrutStyle] is used to establish a predictable vertical layout. /// Since fonts may vary depending on user input and due to font /// fallback, [StrutStyle.forceStrutHeight] is enabled by default /// to lock all lines to the height of the base [TextStyle], provided by /// [style]. This ensures the typed text fits within the allotted space. /// /// If null, the strut used will inherit values from the [style] and will /// have [StrutStyle.forceStrutHeight] set to true. When no [style] is /// passed, the theme's [TextStyle] will be used to generate [strutStyle] /// instead. /// /// To disable strut-based vertical alignment and allow dynamic vertical /// layout based on the glyphs typed, use [StrutStyle.disabled]. /// /// Flutter's strut is based on [typesetting strut](https://en.wikipedia.org/wiki/Strut_(typesetting)) /// and CSS's [line-height](https://www.w3.org/TR/CSS2/visudet.html#line-height). /// {@endtemplate} /// /// Within editable text and text fields, [StrutStyle] will not use its standalone /// default values, and will instead inherit omitted/null properties from the /// [TextStyle] instead. See [StrutStyle.inheritFromTextStyle]. /// /// The user or platform may override this [strutStyle]'s [StrutStyle.height] /// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride]. StrutStyle get strutStyle { if (_strutStyle == null) { return StrutStyle.fromTextStyle(style, forceStrutHeight: true); } return _strutStyle.inheritFromTextStyle(style); } final StrutStyle? _strutStyle; /// {@template flutter.widgets.editableText.textAlign} /// How the text should be aligned horizontally. /// /// Defaults to [TextAlign.start]. /// {@endtemplate} final TextAlign textAlign; /// {@template flutter.widgets.editableText.textDirection} /// The directionality of the text. /// /// This decides how [textAlign] values like [TextAlign.start] and /// [TextAlign.end] are interpreted. /// /// This is also used to disambiguate how to render bidirectional text. For /// example, if the text is an English phrase followed by a Hebrew phrase, /// in a [TextDirection.ltr] context the English phrase will be on the left /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. /// /// Defaults to the ambient [Directionality], if any. /// {@endtemplate} final TextDirection? textDirection; /// {@template flutter.widgets.editableText.textCapitalization} /// Configures how the platform keyboard will select an uppercase or /// lowercase keyboard. /// /// Only supports text keyboards, other keyboard types will ignore this /// configuration. Capitalization is locale-aware. /// /// Defaults to [TextCapitalization.none]. /// /// See also: /// /// * [TextCapitalization], for a description of each capitalization behavior. /// /// {@endtemplate} final TextCapitalization textCapitalization; /// Used to select a font when the same Unicode character can /// be rendered differently, depending on the locale. /// /// It's rarely necessary to set this property. By default its value /// is inherited from the enclosing app with `Localizations.localeOf(context)`. /// /// See [RenderEditable.locale] for more information. final Locale? locale; /// {@template flutter.widgets.editableText.textScaleFactor} /// Deprecated. Will be removed in a future version of Flutter. Use /// [textScaler] instead. /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. /// /// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. /// {@endtemplate} @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 'This feature was deprecated after v3.12.0-2.0.pre.', ) final double? textScaleFactor; /// {@macro flutter.painting.textPainter.textScaler} final TextScaler? textScaler; /// The color to use when painting the cursor. final Color cursorColor; /// The color to use when painting the autocorrection Rect. /// /// For [CupertinoTextField]s, the value is set to the ambient /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the /// value is null on non-iOS platforms and the same color used in [CupertinoTextField] /// on iOS. /// /// Currently the autocorrection Rect only appears on iOS. /// /// Defaults to null, which disables autocorrection Rect painting. final Color? autocorrectionTextRectColor; /// The color to use when painting the background cursor aligned with the text /// while rendering the floating cursor. /// /// Typically this would be set to [CupertinoColors.inactiveGray]. /// /// See also: /// /// * [FloatingCursorDragState], which explains the floating cursor feature /// in detail. final Color backgroundCursorColor; /// {@template flutter.widgets.editableText.maxLines} /// The maximum number of lines to show at one time, wrapping if necessary. /// /// This affects the height of the field itself and does not limit the number /// of lines that can be entered into the field. /// /// If this is 1 (the default), the text will not wrap, but will scroll /// horizontally instead. /// /// If this is null, there is no limit to the number of lines, and the text /// container will start with enough vertical space for one line and /// automatically grow to accommodate additional lines as they are entered, up /// to the height of its constraints. /// /// If this is not null, the value must be greater than zero, and it will lock /// the input to the given number of lines and take up enough horizontal space /// to accommodate that number of lines. Setting [minLines] as well allows the /// input to grow and shrink between the indicated range. /// /// The full set of behaviors possible with [minLines] and [maxLines] are as /// follows. These examples apply equally to [TextField], [TextFormField], /// [CupertinoTextField], and [EditableText]. /// /// Input that occupies a single line and scrolls horizontally as needed. /// ```dart /// const TextField() /// ``` /// /// Input whose height grows from one line up to as many lines as needed for /// the text that was entered. If a height limit is imposed by its parent, it /// will scroll vertically when its height reaches that limit. /// ```dart /// const TextField(maxLines: null) /// ``` /// /// The input's height is large enough for the given number of lines. If /// additional lines are entered the input scrolls vertically. /// ```dart /// const TextField(maxLines: 2) /// ``` /// /// Input whose height grows with content between a min and max. An infinite /// max is possible with `maxLines: null`. /// ```dart /// const TextField(minLines: 2, maxLines: 4) /// ``` /// /// See also: /// /// * [minLines], which sets the minimum number of lines visible. /// {@endtemplate} /// * [expands], which determines whether the field should fill the height of /// its parent. final int? maxLines; /// {@template flutter.widgets.editableText.minLines} /// The minimum number of lines to occupy when the content spans fewer lines. /// /// This affects the height of the field itself and does not limit the number /// of lines that can be entered into the field. /// /// If this is null (default), text container starts with enough vertical space /// for one line and grows to accommodate additional lines as they are entered. /// /// This can be used in combination with [maxLines] for a varying set of behaviors. /// /// If the value is set, it must be greater than zero. If the value is greater /// than 1, [maxLines] should also be set to either null or greater than /// this value. /// /// When [maxLines] is set as well, the height will grow between the indicated /// range of lines. When [maxLines] is null, it will grow as high as needed, /// starting from [minLines]. /// /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows. /// These apply equally to [TextField], [TextFormField], [CupertinoTextField], /// and [EditableText]. /// /// Input that always occupies at least 2 lines and has an infinite max. /// Expands vertically as needed. /// ```dart /// TextField(minLines: 2) /// ``` /// /// Input whose height starts from 2 lines and grows up to 4 lines at which /// point the height limit is reached. If additional lines are entered it will /// scroll vertically. /// ```dart /// const TextField(minLines:2, maxLines: 4) /// ``` /// /// Defaults to null. /// /// See also: /// /// * [maxLines], which sets the maximum number of lines visible, and has /// several examples of how minLines and maxLines interact to produce /// various behaviors. /// {@endtemplate} /// * [expands], which determines whether the field should fill the height of /// its parent. final int? minLines; /// {@template flutter.widgets.editableText.expands} /// Whether this widget's height will be sized to fill its parent. /// /// If set to true and wrapped in a parent widget like [Expanded] or /// [SizedBox], the input will expand to fill the parent. /// /// [maxLines] and [minLines] must both be null when this is set to true, /// otherwise an error is thrown. /// /// Defaults to false. /// /// See the examples in [maxLines] for the complete picture of how [maxLines], /// [minLines], and [expands] interact to produce various behaviors. /// /// Input that matches the height of its parent: /// ```dart /// const Expanded( /// child: TextField(maxLines: null, expands: true), /// ) /// ``` /// {@endtemplate} final bool expands; /// {@template flutter.widgets.editableText.autofocus} /// Whether this text field should focus itself if nothing else is already /// focused. /// /// If true, the keyboard will open as soon as this text field obtains focus. /// Otherwise, the keyboard is only shown after the user taps the text field. /// /// Defaults to false. /// {@endtemplate} // See https://github.com/flutter/flutter/issues/7035 for the rationale for this // keyboard behavior. final bool autofocus; /// The color to use when painting the selection. /// /// If this property is null, this widget gets the selection color from the /// [DefaultSelectionStyle]. /// /// For [CupertinoTextField]s, the value is set to the ambient /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the /// value is set to the ambient [TextSelectionThemeData.selectionColor]. final Color? selectionColor; /// {@template flutter.widgets.editableText.selectionControls} /// Optional delegate for building the text selection handles. /// /// Historically, this field also controlled the toolbar. This is now handled /// by [contextMenuBuilder] instead. However, for backwards compatibility, when /// [selectionControls] is set to an object that does not mix in /// [TextSelectionHandleControls], [contextMenuBuilder] is ignored and the /// [TextSelectionControls.buildToolbar] method is used instead. /// {@endtemplate} /// /// See also: /// /// * [CupertinoTextField], which wraps an [EditableText] and which shows the /// selection toolbar upon user events that are appropriate on the iOS /// platform. /// * [TextField], a Material Design themed wrapper of [EditableText], which /// shows the selection toolbar upon appropriate user events based on the /// user's platform set in [ThemeData.platform]. final TextSelectionControls? selectionControls; /// {@template flutter.widgets.editableText.keyboardType} /// The type of keyboard to use for editing the text. /// /// Defaults to [TextInputType.text] if [maxLines] is one and /// [TextInputType.multiline] otherwise. /// {@endtemplate} final TextInputType keyboardType; /// The type of action button to use with the soft keyboard. final TextInputAction? textInputAction; /// {@template flutter.widgets.editableText.onChanged} /// Called when the user initiates a change to the TextField's /// value: when they have inserted or deleted text. /// /// This callback doesn't run when the TextField's text is changed /// programmatically, via the TextField's [controller]. Typically it /// isn't necessary to be notified of such changes, since they're /// initiated by the app itself. /// /// To be notified of all changes to the TextField's text, cursor, /// and selection, one can add a listener to its [controller] with /// [RichTextEditingController.addListener]. /// /// [onChanged] is called before [onSubmitted] when user indicates completion /// of editing, such as when pressing the "done" button on the keyboard. That /// default behavior can be overridden. See [onEditingComplete] for details. /// /// {@tool dartpad} /// This example shows how onChanged could be used to check the TextField's /// current value each time the user inserts or deletes a character. /// /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart ** /// {@end-tool} /// {@endtemplate} /// /// ## Handling emojis and other complex characters /// {@template flutter.widgets.EditableText.onChanged} /// It's important to always use /// [characters](https://pub.dev/packages/characters) when dealing with user /// input text that may contain complex characters. This will ensure that /// extended grapheme clusters and surrogate pairs are treated as single /// characters, as they appear to the user. /// /// For example, when finding the length of some user input, use /// `string.characters.length`. Do NOT use `string.length` or even /// `string.runes.length`. For the complex character "👨‍👩‍👦", this /// appears to the user as a single character, and `string.characters.length` /// intuitively returns 1. On the other hand, `string.length` returns 8, and /// `string.runes.length` returns 5! /// {@endtemplate} /// /// See also: /// /// * [inputFormatters], which are called before [onChanged] /// runs and can validate and change ("format") the input value. /// * [onEditingComplete], [onSubmitted], [onSelectionChanged]: /// which are more specialized input change notifications. /// * [RichTextEditingController], which implements the [Listenable] interface /// and notifies its listeners on [TextEditingValue] changes. final ValueChanged? onChanged; /// {@template flutter.widgets.editableText.onEditingComplete} /// Called when the user submits editable content (e.g., user presses the "done" /// button on the keyboard). /// /// The default implementation of [onEditingComplete] executes 2 different /// behaviors based on the situation: /// /// - When a completion action is pressed, such as "done", "go", "send", or /// "search", the user's content is submitted to the [controller] and then /// focus is given up. /// /// - When a non-completion action is pressed, such as "next" or "previous", /// the user's content is submitted to the [controller], but focus is not /// given up because developers may want to immediately move focus to /// another input widget within [onSubmitted]. /// /// Providing [onEditingComplete] prevents the aforementioned default behavior. /// {@endtemplate} final VoidCallback? onEditingComplete; /// {@template flutter.widgets.editableText.onSubmitted} /// Called when the user indicates that they are done editing the text in the /// field. /// /// By default, [onSubmitted] is called after [onChanged] when the user /// has finalized editing; or, if the default behavior has been overridden, /// after [onEditingComplete]. See [onEditingComplete] for details. /// /// ## Testing /// The following is the recommended way to trigger [onSubmitted] in a test: /// /// ```dart /// await tester.testTextInput.receiveAction(TextInputAction.done); /// ``` /// /// Sending a `LogicalKeyboardKey.enter` via `tester.sendKeyEvent` will not /// trigger [onSubmitted]. This is because on a real device, the engine /// translates the enter key to a done action, but `tester.sendKeyEvent` sends /// the key to the framework only. /// {@endtemplate} final ValueChanged? onSubmitted; /// {@template flutter.widgets.editableText.onAppPrivateCommand} /// This is used to receive a private command from the input method. /// /// Called when the result of [TextInputClient.performPrivateCommand] is /// received. /// /// This can be used to provide domain-specific features that are only known /// between certain input methods and their clients. /// /// See also: /// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)), /// which is the Android documentation for performPrivateCommand, used to /// send a command from the input method. /// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand), /// which is the Android documentation for sendAppPrivateCommand, used to /// send a command to the input method. /// {@endtemplate} final AppPrivateCommandCallback? onAppPrivateCommand; /// {@template flutter.widgets.editableText.onSelectionChanged} /// Called when the user changes the selection of text (including the cursor /// location). /// {@endtemplate} final SelectionChangedCallback? onSelectionChanged; /// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped} final VoidCallback? onSelectionHandleTapped; /// {@template flutter.widgets.editableText.groupId} /// The group identifier for the [TextFieldTapRegion] of this text field. /// /// Text fields with the same group identifier share the same tap region. /// Defaults to the type of [EditableText]. /// /// See also: /// /// * [TextFieldTapRegion], to give a [groupId] to a widget that is to be /// included in a [EditableText]'s tap region that has [groupId] set. /// {@endtemplate} final Object groupId; /// {@template flutter.widgets.editableText.onTapOutside} /// Called for each tap down that occurs outside of the [TextFieldTapRegion] /// group when the text field is focused. /// /// If this is null, [EditableTextTapOutsideIntent] will be invoked. In the /// default implementation, [FocusNode.unfocus] will be called on the /// [focusNode] for this text field when a [PointerDownEvent] is received on /// another part of the UI. However, it will not unfocus as a result of mobile /// application touch events (which does not include mouse clicks), to conform /// with the platform conventions. To change this behavior, a callback may be /// set here or [EditableTextTapOutsideIntent] may be overridden. /// /// When adding additional controls to a text field (for example, a spinner, a /// button that copies the selected text, or modifies formatting), it is /// helpful if tapping on that control doesn't unfocus the text field. In /// order for an external widget to be considered as part of the text field /// for the purposes of tapping "outside" of the field, wrap the control in a /// [TextFieldTapRegion]. /// /// The [PointerDownEvent] passed to the function is the event that caused the /// notification. It is possible that the event may occur outside of the /// immediate bounding box defined by the text field, although it will be /// within the bounding box of a [TextFieldTapRegion] member. /// {@endtemplate} /// /// {@tool dartpad} /// This example shows how to use a `TextFieldTapRegion` to wrap a set of /// "spinner" buttons that increment and decrement a value in the [TextField] /// without causing the text field to lose keyboard focus. /// /// This example includes a generic `SpinnerField` class that you can copy /// into your own project and customize. /// /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart ** /// {@end-tool} /// /// See also: /// /// * [TapRegion] for how the region group is determined. /// * [onTapUpOutside] which is called for each tap up. /// * [EditableTextTapOutsideIntent] for the intent that is invoked if /// this is null. final TapRegionCallback? onTapOutside; /// {@template flutter.widgets.editableText.onTapUpOutside} /// Called for each tap up that occurs outside of the [TextFieldTapRegion] /// group when the text field is focused. /// /// If this is null, [EditableTextTapUpOutsideIntent] will be invoked. In the /// default implementation, this is a no-op. To change this behavior, set a /// callback here or override [EditableTextTapUpOutsideIntent]. /// /// The [PointerUpEvent] passed to the function is the event that caused the /// notification. It is possible that the event may occur outside of the /// immediate bounding box defined by the text field, although it will be /// within the bounding box of a [TextFieldTapRegion] member. /// {@endtemplate} /// /// See also: /// /// * [TapRegion] for how the region group is determined. /// * [onTapOutside], which is called for each tap down. /// * [EditableTextTapOutsideIntent], the intent that is invoked if /// this is null. final TapRegionUpCallback? onTapUpOutside; /// {@template flutter.widgets.editableText.inputFormatters} /// Optional input validation and formatting overrides. /// /// Formatters are run in the provided order when the user changes the text /// this widget contains. When this parameter changes, the new formatters will /// not be applied until the next time the user inserts or deletes text. /// Similar to the [onChanged] callback, formatters don't run when the text is /// changed programmatically via [controller]. /// /// See also: /// /// * [RichTextEditingController], which implements the [Listenable] interface /// and notifies its listeners on [TextEditingValue] changes. /// {@endtemplate} final List? inputFormatters; /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// /// If this property is null, [SystemMouseCursors.text] will be used. /// /// The [mouseCursor] is the only property of [EditableText] that controls the /// appearance of the mouse pointer. All other properties related to "cursor" /// stands for the text cursor, which is usually a blinking vertical line at /// the editing position. final MouseCursor? mouseCursor; /// Whether the caller will provide gesture handling (true), or if the /// [EditableText] is expected to handle basic gestures (false). /// /// When this is false, the [EditableText] (or more specifically, the /// [RenderEditable]) enables some rudimentary gestures (tap to position the /// cursor, long-press to select all, and some scrolling behavior). /// /// These behaviors are sufficient for debugging purposes but are inadequate /// for user-facing applications. To enable platform-specific behaviors, use a /// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set /// [rendererIgnoresPointer] to true. /// /// When [rendererIgnoresPointer] is true, the [RenderEditable] created /// by this widget will not handle pointer events. /// /// This property is false by default. /// /// See also: /// /// * [RenderEditable.ignorePointer], which implements this feature. /// * [TextSelectionGestureDetectorBuilder], which implements platform-specific /// gestures and behaviors. final bool rendererIgnoresPointer; /// {@template flutter.widgets.editableText.cursorWidth} /// How thick the cursor will be. /// /// Defaults to 2.0. /// /// The cursor will draw under the text. The cursor width will extend /// to the right of the boundary between characters for left-to-right text /// and to the left for right-to-left text. This corresponds to extending /// downstream relative to the selected position. Negative values may be used /// to reverse this behavior. /// {@endtemplate} final double cursorWidth; /// {@template flutter.widgets.editableText.cursorHeight} /// How tall the cursor will be. /// /// If this property is null, [RenderEditable.preferredLineHeight] will be used. /// {@endtemplate} final double? cursorHeight; /// {@template flutter.widgets.editableText.cursorRadius} /// How rounded the corners of the cursor should be. /// /// By default, the cursor has no radius. /// {@endtemplate} final Radius? cursorRadius; /// {@template flutter.widgets.editableText.cursorOpacityAnimates} /// Whether the cursor will animate from fully transparent to fully opaque /// during each cursor blink. /// /// By default, the cursor opacity will animate on iOS platforms and will not /// animate on Android platforms. /// {@endtemplate} final bool cursorOpacityAnimates; /// {@macro flutter.rendering.RenderEditable.cursorOffset} final Offset? cursorOffset; /// {@macro flutter.rendering.RenderEditable.paintCursorAboveText} final bool paintCursorAboveText; /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. final ui.BoxHeightStyle selectionHeightStyle; /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. final ui.BoxWidthStyle selectionWidthStyle; /// The appearance of the keyboard. /// /// This setting is only honored on iOS devices. /// /// Defaults to [Brightness.light]. final Brightness keyboardAppearance; /// {@template flutter.widgets.editableText.scrollPadding} /// Configures the padding for the edges surrounding a [Scrollable] when the /// text field scrolls into view. /// /// When this widget receives focus and is not completely visible (for example /// scrolled partially off the screen or overlapped by the keyboard), then it /// will attempt to make itself visible by scrolling a surrounding /// [Scrollable], if one is present. This value controls how far from the /// edges of a [Scrollable] the TextField will be positioned after the scroll. /// /// Defaults to EdgeInsets.all(20.0). /// {@endtemplate} final EdgeInsets scrollPadding; /// {@template flutter.widgets.editableText.enableInteractiveSelection} /// Whether to enable user interface affordances for changing the /// text selection. /// /// For example, setting this to true will enable features such as /// long-pressing the TextField to select text and show the /// cut/copy/paste menu, and tapping to move the text caret. /// /// When this is false, the text selection cannot be adjusted by /// the user, the cut/copy/paste menu is hidden, and the shortcuts to /// cut/copy/paste text do nothing but stop propagation of the key event /// to other key event handlers in the focus chain. /// /// Defaults to true. /// {@endtemplate} final bool enableInteractiveSelection; /// Setting this property to true makes the cursor stop blinking or fading /// on and off once the cursor appears on focus. This property is useful for /// testing purposes. /// /// It does not affect the necessity to focus the EditableText for the cursor /// to appear in the first place. /// /// Defaults to false, resulting in a typical blinking cursor. static bool debugDeterministicCursor = false; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@template flutter.widgets.editableText.scrollController} /// The [ScrollController] to use when vertically scrolling the input. /// /// If null, it will instantiate a new ScrollController. /// /// See [Scrollable.controller]. /// {@endtemplate} final ScrollController? scrollController; /// {@template flutter.widgets.editableText.scrollPhysics} /// The [ScrollPhysics] to use when vertically scrolling the input. /// /// If not specified, it will behave according to the current platform. /// /// See [Scrollable.physics]. /// {@endtemplate} /// /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the /// [ScrollPhysics] provided by that behavior will take precedence after /// [scrollPhysics]. final ScrollPhysics? scrollPhysics; /// {@template flutter.widgets.editableText.scribbleEnabled} /// Whether iOS 14 Scribble features are enabled for this widget. /// /// Only available on iPads. /// /// Defaults to true. /// {@endtemplate} @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) final bool scribbleEnabled; /// {@template flutter.widgets.editableText.stylusHandwritingEnabled} /// Whether this input supports stylus handwriting, where the user can write /// directly on top of a field. /// /// Currently only the following devices are supported: /// /// * iPads running iOS 14 and above using an Apple Pencil. /// * Android devices running API 34 and above and using an active stylus. /// {@endtemplate} /// /// On Android, Scribe gestures are detected outside of [EditableText], /// typically by [TextSelectionGestureDetectorBuilder]. This is handled /// automatically in [TextField]. /// /// See also: /// /// * [ScribbleClient], which can be mixed into an arbitrary widget to /// provide iOS Scribble functionality. /// * [Scribe], which can be used to interact with Android Scribe directly. final bool stylusHandwritingEnabled; /// {@template flutter.widgets.editableText.selectionEnabled} /// Same as [enableInteractiveSelection]. /// /// This getter exists primarily for consistency with /// [RenderEditable.selectionEnabled]. /// {@endtemplate} bool get selectionEnabled => enableInteractiveSelection; /// {@template flutter.widgets.editableText.selectAllOnFocus} /// Whether this field should select all text when gaining focus. /// /// When false, focusing this text field will leave its /// existing text selection unchanged. /// /// Defaults to true on web and desktop platforms, and false on mobile platforms. /// {@endtemplate} final bool selectAllOnFocus; /// {@template flutter.widgets.editableText.autofillHints} /// A list of strings that helps the autofill service identify the type of this /// text input. /// /// When set to null, this text input will not send its autofill information /// to the platform, preventing it from participating in autofills triggered /// by a different [AutofillClient], even if they're in the same /// [AutofillScope]. Additionally, on Android and web, setting this to null /// will disable autofill for this text field. /// /// The minimum platform SDK version that supports Autofill is API level 26 /// for Android, and iOS 10.0 for iOS. /// /// Defaults to an empty list. /// /// ### Setting up iOS autofill: /// /// To provide the best user experience and ensure your app fully supports /// password autofill on iOS, follow these steps: /// /// * Set up your iOS app's /// [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app). /// * Some autofill hints only work with specific [keyboardType]s. For example, /// [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email] /// works only with [TextInputType.emailAddress]. Make sure the input field has a /// compatible [keyboardType]. Empirically, [TextInputType.name] works well /// with many autofill hints that are predefined on iOS. /// /// ### Troubleshooting Autofill /// /// Autofill service providers rely heavily on [autofillHints]. Make sure the /// entries in [autofillHints] are supported by the autofill service currently /// in use (the name of the service can typically be found in your mobile /// device's system settings). /// /// #### Autofill UI refuses to show up when I tap on the text field /// /// Check the device's system settings and make sure autofill is turned on, /// and there are available credentials stored in the autofill service. /// /// * iOS password autofill: Go to Settings -> Password, turn on "Autofill /// Passwords", and add new passwords for testing by pressing the top right /// "+" button. Use an arbitrary "website" if you don't have associated /// domains set up for your app. As long as there's at least one password /// stored, you should be able to see a key-shaped icon in the quick type /// bar on the software keyboard, when a password related field is focused. /// /// * iOS contact information autofill: iOS seems to pull contact info from /// the Apple ID currently associated with the device. Go to Settings -> /// Apple ID (usually the first entry, or "Sign in to your iPhone" if you /// haven't set up one on the device), and fill out the relevant fields. If /// you wish to test more contact info types, try adding them in Contacts -> /// My Card. /// /// * Android autofill: Go to Settings -> System -> Languages & input -> /// Autofill service. Enable the autofill service of your choice, and make /// sure there are available credentials associated with your app. /// /// Specifying [InputDecoration.hintText] may also help autofill services /// (like Samsung Pass) determine the expected content type of an input field, /// although this is typically not required when autofillHints are present. /// /// #### I called `TextInput.finishAutofillContext` but the autofill save /// prompt isn't showing /// /// * iOS: iOS may not show a prompt or any other visual indication when it /// saves user password. Go to Settings -> Password and check if your new /// password is saved. Neither saving password nor auto-generating strong /// password works without properly setting up associated domains in your /// app. To set up associated domains, follow the instructions in /// . /// /// {@endtemplate} /// {@macro flutter.services.AutofillConfiguration.autofillHints} final Iterable? autofillHints; /// The [AutofillClient] that controls this input field's autofill behavior. /// /// When null, this widget's [EditableTextState] will be used as the /// [AutofillClient]. This property may override [autofillHints]. final AutofillClient? autofillClient; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// Restoration ID to save and restore the scroll offset of the /// [EditableText]. /// /// If a restoration id is provided, the [EditableText] will persist its /// current scroll offset and restore it during state restoration. /// /// The scroll offset is persisted in a [RestorationBucket] claimed from /// the surrounding [RestorationScope] using the provided restoration ID. /// /// Persisting and restoring the content of the [EditableText] is the /// responsibility of the owner of the [controller], who may use a /// [RestorableRichTextEditingController] for that purpose. /// /// See also: /// /// * [RestorationManager], which explains how state restoration works in /// Flutter. final String? restorationId; /// {@template flutter.widgets.editableText.scrollBehavior} /// A [ScrollBehavior] that will be applied to this widget individually. /// /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and /// modified to alter the viewport decoration, like [Scrollbar]s. /// /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit /// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence, /// followed by [scrollBehavior], and then the inherited ancestor /// [ScrollBehavior]. /// {@endtemplate} /// /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be /// modified by default to only apply a [Scrollbar] if [maxLines] is greater /// than 1. final ScrollBehavior? scrollBehavior; /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; /// {@template flutter.widgets.editableText.contentInsertionConfiguration} /// Configuration of handler for media content inserted via the system input /// method. /// /// Defaults to null in which case media content insertion will be disabled, /// and the system will display a message informing the user that the text field /// does not support inserting media content. /// /// Set [ContentInsertionConfiguration.onContentInserted] to provide a handler. /// Additionally, set [ContentInsertionConfiguration.allowedMimeTypes] /// to limit the allowable mime types for inserted content. /// /// {@tool dartpad} /// /// This example shows how to access the data for inserted content in your /// `TextField`. /// /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** /// {@end-tool} /// /// If [contentInsertionConfiguration] is not provided, by default /// an empty list of mime types will be sent to the Flutter Engine. /// A handler function must be provided in order to customize the allowable /// mime types for inserted content. /// /// If rich content is inserted without a handler, the system will display /// a message informing the user that the current text input does not support /// inserting rich content. /// {@endtemplate} final ContentInsertionConfiguration? contentInsertionConfiguration; /// {@template flutter.widgets.EditableText.contextMenuBuilder} /// Builds the text selection toolbar when requested by the user. /// /// The context menu is built when [EditableTextState.showToolbar] is called, /// typically by one of the callbacks installed by the widget created by /// [TextSelectionGestureDetectorBuilder.buildGestureDetector]. The widget /// returned by [contextMenuBuilder] is passed to a [ContextMenuController]. /// /// If no callback is provided, no context menu will be shown. /// /// The [EditableTextContextMenuBuilder] signature used by the /// [contextMenuBuilder] callback has two parameters, the [BuildContext] of /// the [EditableText] and the [EditableTextState] of the [EditableText]. /// /// The [EditableTextState] has two properties that are especially useful when /// building the widgets for the context menu: /// /// * [EditableTextState.contextMenuAnchors] specifies the desired anchor /// position for the context menu. /// /// * [EditableTextState.contextMenuButtonItems] represents the buttons that /// should typically be built for this widget (e.g. cut, copy, paste). /// /// The [TextSelectionToolbarLayoutDelegate] class may be particularly useful /// in honoring the preferred anchor positions. /// /// For backwards compatibility, when [EditableText.selectionControls] is set /// to an object that does not mix in [TextSelectionHandleControls], /// [contextMenuBuilder] is ignored and the /// [TextSelectionControls.buildToolbar] method is used instead. /// /// {@tool dartpad} /// This example shows how to customize the menu, in this case by keeping the /// default buttons for the platform but modifying their appearance. /// /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This example shows how to show a custom button only when an email address /// is currently selected. /// /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart ** /// {@end-tool} /// /// See also: /// * [AdaptiveTextSelectionToolbar], which builds the default text selection /// toolbar for the current platform, but allows customization of the /// buttons. /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the /// button Widgets for the current platform given /// [ContextMenuButtonItem]s. /// * [BrowserContextMenu], which allows the browser's context menu on web /// to be disabled and Flutter-rendered context menus to appear. /// {@endtemplate} final EditableTextContextMenuBuilder? contextMenuBuilder; /// {@template flutter.widgets.EditableText.spellCheckConfiguration} /// Configuration that details how spell check should be performed. /// /// Specifies the [SpellCheckService] used to spell check text input and the /// [TextStyle] used to style text with misspelled words. /// /// If the [SpellCheckService] is left null, spell check is disabled by /// default unless the [DefaultSpellCheckService] is supported, in which case /// it is used. It is currently supported only on Android and iOS. /// /// If this configuration is left null, then spell check is disabled by default. /// {@endtemplate} final SpellCheckConfiguration? spellCheckConfiguration; /// The configuration for the magnifier to use with selections in this text /// field. /// /// {@macro flutter.widgets.magnifier.intro} final TextMagnifierConfiguration magnifierConfiguration; /// {@macro flutter.services.TextInputConfiguration.hintLocales} final List? hintLocales; /// The default value for [selectionHeightStyle]. /// /// On web platforms, this defaults to [ui.BoxHeightStyle.max]. /// /// On native platforms, this defaults to [ui.BoxHeightStyle.includeLineSpacingMiddle] for all /// platforms. static ui.BoxHeightStyle get defaultSelectionHeightStyle { if (kIsWeb) { return ui.BoxHeightStyle.max; } return ui.BoxHeightStyle.includeLineSpacingMiddle; } /// The default value for [selectionWidthStyle]. /// /// On web platforms, this defaults to [ui.BoxWidthStyle.max] for Apple platforms running /// Safari (webkit) based browsers and [ui.BoxWidthStyle.tight] for all others. /// /// On non-web platforms, this defaults to [ui.BoxWidthStyle.max]. static ui.BoxWidthStyle get defaultSelectionWidthStyle { // if (kIsWeb) { // if (defaultTargetPlatform == TargetPlatform.iOS || // WebBrowserDetection.isSafari) { // // On macOS web, the selection width behavior differs when running on // // Chrom(e|ium) (blink) or Safari (webkit). // return ui.BoxWidthStyle.max; // } // return ui.BoxWidthStyle.tight; // } return ui.BoxWidthStyle.max; } /// The default value for [stylusHandwritingEnabled]. static const bool defaultStylusHandwritingEnabled = true; bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText); /// The default value for [selectAllOnFocus]. static bool get _defaultSelectAllOnFocus { if (kIsWeb) { return true; } return switch (defaultTargetPlatform) { TargetPlatform.android => false, TargetPlatform.iOS => false, TargetPlatform.fuchsia => false, TargetPlatform.linux => true, TargetPlatform.macOS => true, TargetPlatform.windows => true, }; } /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu for an editable field. /// /// For example, [EditableText] uses this to generate the default buttons for /// its context menu. /// /// See also: /// /// * [EditableTextState.contextMenuButtonItems], which gives the /// [ContextMenuButtonItem]s for a specific EditableText. /// * [SelectableRegion.getSelectableButtonItems], which performs a similar /// role but for content that is selectable but not editable. /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can /// take a list of [ContextMenuButtonItem]s with /// [AdaptiveTextSelectionToolbar.buttonItems]. /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button /// Widgets for the current platform given [ContextMenuButtonItem]s. static List getEditableButtonItems({ required final ClipboardStatus? clipboardStatus, required final VoidCallback? onCopy, required final VoidCallback? onCut, required final VoidCallback? onPaste, required final VoidCallback? onSelectAll, required final VoidCallback? onLookUp, required final VoidCallback? onSearchWeb, required final VoidCallback? onShare, required final VoidCallback? onLiveTextInput, }) { final resultButtonItem = []; // Configure button items with clipboard. if (onPaste == null || clipboardStatus != ClipboardStatus.unknown) { // If the paste button is enabled, don't render anything until the state // of the clipboard is known, since it's used to determine if paste is // shown. // On Android, the share button is before the select all button. final showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android; resultButtonItem.addAll([ if (onCut != null) ContextMenuButtonItem( onPressed: onCut, type: ContextMenuButtonType.cut, ), if (onCopy != null) ContextMenuButtonItem( onPressed: onCopy, type: ContextMenuButtonType.copy, ), if (onPaste != null) ContextMenuButtonItem( onPressed: onPaste, type: ContextMenuButtonType.paste, ), if (onShare != null && showShareBeforeSelectAll) ContextMenuButtonItem( onPressed: onShare, type: ContextMenuButtonType.share, ), if (onSelectAll != null) ContextMenuButtonItem( onPressed: onSelectAll, type: ContextMenuButtonType.selectAll, ), if (onLookUp != null) ContextMenuButtonItem( onPressed: onLookUp, type: ContextMenuButtonType.lookUp, ), if (onSearchWeb != null) ContextMenuButtonItem( onPressed: onSearchWeb, type: ContextMenuButtonType.searchWeb, ), if (onShare != null && !showShareBeforeSelectAll) ContextMenuButtonItem( onPressed: onShare, type: ContextMenuButtonType.share, ), ]); } // Config button items with Live Text. if (onLiveTextInput != null) { resultButtonItem.add( ContextMenuButtonItem( onPressed: onLiveTextInput, type: ContextMenuButtonType.liveTextInput, ), ); } return resultButtonItem; } // Infer the value of autocorrect from autofillHints. static bool _inferAutocorrect({required Iterable? autofillHints}) { if (autofillHints == null || autofillHints.isEmpty || kIsWeb) { return true; } switch (defaultTargetPlatform) { case TargetPlatform.iOS: // username, password and newPassword are password related hint. // newUsername is not supported on iOS. final bool passwordRelatedHint = autofillHints.any( (String hint) => hint == AutofillHints.username || hint == AutofillHints.password || hint == AutofillHints.newPassword, ); if (passwordRelatedHint) { // https://github.com/flutter/flutter/issues/134723 // Set autocorrect to false to prevent password bar from flashing. return false; } case TargetPlatform.macOS: case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: break; } return true; } // Infer the keyboard type of an `EditableText` if it's not specified. static TextInputType _inferKeyboardType({ required Iterable? autofillHints, required int? maxLines, }) { if (autofillHints == null || autofillHints.isEmpty) { return maxLines == 1 ? TextInputType.text : TextInputType.multiline; } final String effectiveHint = autofillHints.first; // On iOS oftentimes specifying a text content type is not enough to qualify // the input field for autofill. The keyboard type also needs to be compatible // with the content type. To get autofill to work by default on EditableText, // the keyboard type inference on iOS is done differently from other platforms. // // The entries with "autofill not working" comments are the iOS text content // types that should work with the specified keyboard type but won't trigger // (even within a native app). Tested on iOS 13.5. if (!kIsWeb) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: const iOSKeyboardType = { AutofillHints.addressCity: TextInputType.name, AutofillHints.addressCityAndState: TextInputType.name, // Autofill not working. AutofillHints.addressState: TextInputType.name, AutofillHints.countryName: TextInputType.name, AutofillHints.creditCardNumber: TextInputType.number, // Couldn't test. AutofillHints.email: TextInputType.emailAddress, AutofillHints.familyName: TextInputType.name, AutofillHints.fullStreetAddress: TextInputType.name, AutofillHints.givenName: TextInputType.name, AutofillHints.jobTitle: TextInputType.name, // Autofill not working. AutofillHints.location: TextInputType.name, // Autofill not working. AutofillHints.middleName: TextInputType.name, // Autofill not working. AutofillHints.name: TextInputType.name, AutofillHints.namePrefix: TextInputType.name, // Autofill not working. AutofillHints.nameSuffix: TextInputType.name, // Autofill not working. AutofillHints.newPassword: TextInputType.text, AutofillHints.newUsername: TextInputType.text, AutofillHints.nickname: TextInputType.name, // Autofill not working. AutofillHints.oneTimeCode: TextInputType.number, AutofillHints.organizationName: TextInputType.text, // Autofill not working. AutofillHints.password: TextInputType.text, AutofillHints.postalCode: TextInputType.name, AutofillHints.streetAddressLine1: TextInputType.name, AutofillHints.streetAddressLine2: TextInputType.name, // Autofill not working. AutofillHints.sublocality: TextInputType.name, // Autofill not working. AutofillHints.telephoneNumber: TextInputType.name, AutofillHints.url: TextInputType.url, // Autofill not working. AutofillHints.username: TextInputType.text, }; final TextInputType? keyboardType = iOSKeyboardType[effectiveHint]; if (keyboardType != null) { return keyboardType; } case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: break; } } if (maxLines != 1) { return TextInputType.multiline; } const inferKeyboardType = { AutofillHints.addressCity: TextInputType.streetAddress, AutofillHints.addressCityAndState: TextInputType.streetAddress, AutofillHints.addressState: TextInputType.streetAddress, AutofillHints.birthday: TextInputType.datetime, AutofillHints.birthdayDay: TextInputType.datetime, AutofillHints.birthdayMonth: TextInputType.datetime, AutofillHints.birthdayYear: TextInputType.datetime, AutofillHints.countryCode: TextInputType.number, AutofillHints.countryName: TextInputType.text, AutofillHints.creditCardExpirationDate: TextInputType.datetime, AutofillHints.creditCardExpirationDay: TextInputType.datetime, AutofillHints.creditCardExpirationMonth: TextInputType.datetime, AutofillHints.creditCardExpirationYear: TextInputType.datetime, AutofillHints.creditCardFamilyName: TextInputType.name, AutofillHints.creditCardGivenName: TextInputType.name, AutofillHints.creditCardMiddleName: TextInputType.name, AutofillHints.creditCardName: TextInputType.name, AutofillHints.creditCardNumber: TextInputType.number, AutofillHints.creditCardSecurityCode: TextInputType.number, AutofillHints.creditCardType: TextInputType.text, AutofillHints.email: TextInputType.emailAddress, AutofillHints.familyName: TextInputType.name, AutofillHints.fullStreetAddress: TextInputType.streetAddress, AutofillHints.gender: TextInputType.text, AutofillHints.givenName: TextInputType.name, AutofillHints.impp: TextInputType.url, AutofillHints.jobTitle: TextInputType.text, AutofillHints.language: TextInputType.text, AutofillHints.location: TextInputType.streetAddress, AutofillHints.middleInitial: TextInputType.name, AutofillHints.middleName: TextInputType.name, AutofillHints.name: TextInputType.name, AutofillHints.namePrefix: TextInputType.name, AutofillHints.nameSuffix: TextInputType.name, AutofillHints.newPassword: TextInputType.text, AutofillHints.newUsername: TextInputType.text, AutofillHints.nickname: TextInputType.text, AutofillHints.oneTimeCode: TextInputType.text, AutofillHints.organizationName: TextInputType.text, AutofillHints.password: TextInputType.text, AutofillHints.photo: TextInputType.text, AutofillHints.postalAddress: TextInputType.streetAddress, AutofillHints.postalAddressExtended: TextInputType.streetAddress, AutofillHints.postalAddressExtendedPostalCode: TextInputType.number, AutofillHints.postalCode: TextInputType.number, AutofillHints.streetAddressLevel1: TextInputType.streetAddress, AutofillHints.streetAddressLevel2: TextInputType.streetAddress, AutofillHints.streetAddressLevel3: TextInputType.streetAddress, AutofillHints.streetAddressLevel4: TextInputType.streetAddress, AutofillHints.streetAddressLine1: TextInputType.streetAddress, AutofillHints.streetAddressLine2: TextInputType.streetAddress, AutofillHints.streetAddressLine3: TextInputType.streetAddress, AutofillHints.sublocality: TextInputType.streetAddress, AutofillHints.telephoneNumber: TextInputType.phone, AutofillHints.telephoneNumberAreaCode: TextInputType.phone, AutofillHints.telephoneNumberCountryCode: TextInputType.phone, AutofillHints.telephoneNumberDevice: TextInputType.phone, AutofillHints.telephoneNumberExtension: TextInputType.phone, AutofillHints.telephoneNumberLocal: TextInputType.phone, AutofillHints.telephoneNumberLocalPrefix: TextInputType.phone, AutofillHints.telephoneNumberLocalSuffix: TextInputType.phone, AutofillHints.telephoneNumberNational: TextInputType.phone, AutofillHints.transactionAmount: TextInputType.numberWithOptions( decimal: true, ), AutofillHints.transactionCurrency: TextInputType.text, AutofillHints.url: TextInputType.url, AutofillHints.username: TextInputType.text, }; return inferKeyboardType[effectiveHint] ?? TextInputType.text; } @override EditableTextState createState() => EditableTextState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( DiagnosticsProperty( 'controller', controller, ), ) ..add(DiagnosticsProperty('focusNode', focusNode)) ..add( DiagnosticsProperty( 'obscureText', obscureText, defaultValue: false, ), ) ..add( DiagnosticsProperty('readOnly', readOnly, defaultValue: false), ) ..add( DiagnosticsProperty( 'autocorrect', autocorrect, defaultValue: null, ), ) ..add( EnumProperty( 'smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, ), ) ..add( EnumProperty( 'smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, ), ) ..add( DiagnosticsProperty( 'enableSuggestions', enableSuggestions, defaultValue: true, ), ); style.debugFillProperties(properties); properties ..add( EnumProperty('textAlign', textAlign, defaultValue: null), ) ..add( EnumProperty( 'textDirection', textDirection, defaultValue: null, ), ) ..add( DiagnosticsProperty('locale', locale, defaultValue: null), ) ..add( DiagnosticsProperty( 'textScaler', textScaler, defaultValue: null, ), ) ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) ..add(IntProperty('minLines', minLines, defaultValue: null)) ..add( DiagnosticsProperty('expands', expands, defaultValue: false), ) ..add( DiagnosticsProperty('autofocus', autofocus, defaultValue: false), ) ..add( DiagnosticsProperty( 'keyboardType', keyboardType, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollController', scrollController, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollPhysics', scrollPhysics, defaultValue: null, ), ) ..add( DiagnosticsProperty>( 'autofillHints', autofillHints, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'textHeightBehavior', textHeightBehavior, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scribbleEnabled', scribbleEnabled, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'stylusHandwritingEnabled', stylusHandwritingEnabled, defaultValue: defaultStylusHandwritingEnabled, ), ) ..add( DiagnosticsProperty( 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'enableInteractiveSelection', enableInteractiveSelection, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'spellCheckConfiguration', spellCheckConfiguration, defaultValue: null, ), ) ..add( DiagnosticsProperty>( 'contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const [], defaultValue: contentInsertionConfiguration == null ? const [] : kDefaultContentInsertionMimeTypes, ), ) ..add( DiagnosticsProperty?>( 'hintLocales', hintLocales, defaultValue: null, ), ); } } /// State for an [EditableText]. class EditableTextState extends State with AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, TextSelectionDelegate, TextInputClient, DeltaTextInputClient implements AutofillClient { Timer? _cursorTimer; AnimationController get _cursorBlinkOpacityController { return _backingCursorBlinkOpacityController ??= AnimationController( vsync: this, )..addListener(_onCursorColorTick); } AnimationController? _backingCursorBlinkOpacityController; late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation.iOSBlinkingCaret(); final ValueNotifier _cursorVisibilityNotifier = ValueNotifier( true, ); final GlobalKey _editableKey = GlobalKey(); /// Detects whether the clipboard can paste. final ClipboardStatusNotifier clipboardStatus = kIsWeb // Web browsers will show a permission dialog when Clipboard.hasStrings is // called. In an EditableText, this will happen before the paste button is // clicked, often before the context menu is even shown. To avoid this // poor user experience, always show the paste button on web. ? _WebClipboardStatusNotifier() : ClipboardStatusNotifier(); /// Detects whether the Live Text input is enabled. /// /// See also: /// * [LiveText], where the availability of Live Text input can be obtained. final LiveTextInputStatusNotifier? _liveTextInputStatus = kIsWeb ? null : LiveTextInputStatusNotifier(); TextInputConnection? _textInputConnection; bool get _hasInputConnection => _textInputConnection?.attached ?? false; TextSelectionOverlay? _selectionOverlay; ScrollNotificationObserverState? _scrollNotificationObserver; ({TextEditingValue value, Rect selectionBounds})? _dataWhenToolbarShowScheduled; bool _listeningToScrollNotificationObserver = false; bool get _webContextMenuEnabled => kIsWeb && BrowserContextMenu.enabled; final GlobalKey _scrollableKey = GlobalKey(); ScrollController? _internalScrollController; ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController()); final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _endHandleLayerLink = LayerLink(); bool _didAutoFocus = false; AutofillGroupState? _currentAutofillScope; @override AutofillScope? get currentAutofillScope => _currentAutofillScope; AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this; late SpellCheckConfiguration _spellCheckConfiguration; late TextStyle _style; /// Configuration that determines how spell check will be performed. /// /// If possible, this configuration will contain a default for the /// [SpellCheckService] if it is not otherwise specified. /// /// See also: /// * [DefaultSpellCheckService], the spell check service used by default. @visibleForTesting SpellCheckConfiguration get spellCheckConfiguration => _spellCheckConfiguration; /// Whether or not spell check is enabled. /// /// Spell check is enabled when a [SpellCheckConfiguration] has been specified /// for the widget. bool get spellCheckEnabled => _spellCheckConfiguration.spellCheckEnabled; /// The most up-to-date spell check results for text input. /// /// These results will be updated via calls to spell check through a /// [SpellCheckService] and used by this widget to build the [TextSpan] tree /// for text input and menus for replacement suggestions of misspelled words. SpellCheckResults? spellCheckResults; bool get _spellCheckResultsReceived => spellCheckEnabled && spellCheckResults != null && spellCheckResults!.suggestionSpans.isNotEmpty; /// The text processing service used to retrieve the native text processing actions. final ProcessTextService _processTextService = DefaultProcessTextService(); /// The list of native text processing actions provided by the engine. final List _processTextActions = []; /// Whether to create an input connection with the platform for text editing /// or not. /// /// Read-only input fields do not need a connection with the platform since /// there's no need for text editing capabilities (e.g. virtual keyboard). /// /// On macOS, most of the selection and focus related shortcuts require a /// connection with the platform because appropriate platform selectors are /// sent from the engine and translated into intents. For read-only fields /// those shortcuts should be available (for instance to allow tab traversal). /// /// On the web, we always need a connection because we want some browser /// functionalities to continue to work on read-only input fields like: /// - Relevant context menu. /// - cmd/ctrl+c shortcut to copy. /// - cmd/ctrl+a to select all. /// - Changing the selection using a physical keyboard. bool get _shouldCreateInputConnection => kIsWeb || defaultTargetPlatform == TargetPlatform.macOS || !widget.readOnly; // The time it takes for the floating cursor to snap to the text aligned // cursor position after the user has finished placing it. static const Duration _floatingCursorResetTime = Duration(milliseconds: 125); AnimationController? _floatingCursorResetController; Orientation? _lastOrientation; bool get _stylusHandwritingEnabled { // During the deprecation period, respect scribbleEnabled being explicitly // set. if (!widget.scribbleEnabled) { return widget.scribbleEnabled; } return widget.stylusHandwritingEnabled; } late final AppLifecycleListener _appLifecycleListener; bool _justResumed = false; @override bool get wantKeepAlive => widget.focusNode.hasFocus; Color get _cursorColor { final double effectiveOpacity = math.min( widget.cursorColor.alpha / 255.0, _cursorBlinkOpacityController.value, ); return widget.cursorColor.withValues(alpha: effectiveOpacity); } @override bool get cutEnabled { if (widget.selectionControls is! TextSelectionHandleControls) { return widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText; } return !widget.readOnly && !widget.obscureText && !textEditingValue.selection.isCollapsed; } @override bool get copyEnabled { if (widget.selectionControls is! TextSelectionHandleControls) { return widget.toolbarOptions.copy && !widget.obscureText; } return !widget.obscureText && !textEditingValue.selection.isCollapsed; } @override bool get pasteEnabled { if (widget.selectionControls is! TextSelectionHandleControls) { return widget.toolbarOptions.paste && !widget.readOnly; } return !widget.readOnly && (clipboardStatus.value == ClipboardStatus.pasteable); } @override bool get selectAllEnabled { if (widget.selectionControls is! TextSelectionHandleControls) { return widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection; } if (!widget.enableInteractiveSelection || (widget.readOnly && widget.obscureText)) { return false; } switch (defaultTargetPlatform) { case TargetPlatform.macOS: return false; case TargetPlatform.iOS: return textEditingValue.text.isNotEmpty && textEditingValue.selection.isCollapsed; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return textEditingValue.text.isNotEmpty && !(textEditingValue.selection.start == 0 && textEditingValue.selection.end == textEditingValue.text.length); } } @override bool get lookUpEnabled { if (defaultTargetPlatform != TargetPlatform.iOS) { return false; } return !widget.obscureText && !textEditingValue.selection.isCollapsed && textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; } @override bool get searchWebEnabled { if (defaultTargetPlatform != TargetPlatform.iOS) { return false; } return !widget.obscureText && !textEditingValue.selection.isCollapsed && textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; } @override bool get shareEnabled { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: return !widget.obscureText && !textEditingValue.selection.isCollapsed && textEditingValue.selection .textInside(textEditingValue.text) .trim() != ''; case TargetPlatform.macOS: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return false; } } @override bool get liveTextInputEnabled { return _liveTextInputStatus?.value == LiveTextInputStatus.enabled && !widget.obscureText && !widget.readOnly && textEditingValue.selection.isCollapsed; } void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. }); } void _onChangedLiveTextInputStatus() { setState(() { // Inform the widget that the value of liveTextInputStatus has changed. }); } TextEditingValue get _textEditingValueforTextLayoutMetrics { final Widget? editableWidget = _editableKey.currentContext?.widget; if (editableWidget is! _Editable) { throw StateError('_Editable must be mounted.'); } return editableWidget.value; } /// Copy current selection to [Clipboard]. @override void copySelection(SelectionChangedCause cause) { final TextSelection selection = textEditingValue.selection; if (selection.isCollapsed || widget.obscureText) { return; } // bggRGjQaUbCoE copySelection final String text = widget.controller.getSelectionText(selection) ?? selection.textInside(textEditingValue.text); Clipboard.setData(ClipboardData(text: text)); if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); hideToolbar(false); switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: break; case TargetPlatform.android: case TargetPlatform.fuchsia: // Collapse the selection and hide the toolbar and handles. userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, selection: TextSelection.collapsed( offset: textEditingValue.selection.end, ), ), SelectionChangedCause.toolbar, ); } } clipboardStatus.update(); } /// Cut current selection to [Clipboard]. @override void cutSelection(SelectionChangedCause cause) { if (widget.readOnly || widget.obscureText) { return; } final TextSelection selection = textEditingValue.selection; if (selection.isCollapsed) { return; } // bggRGjQaUbCoE cutSelection final String text = widget.controller.getSelectionText(selection) ?? selection.textInside(textEditingValue.text); Clipboard.setData(ClipboardData(text: text)); _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); if (cause == SelectionChangedCause.toolbar) { // Schedule a call to bringIntoView() after renderEditable updates. SchedulerBinding.instance.addPostFrameCallback((_) { if (mounted) { bringIntoView(textEditingValue.selection.extent); } }, debugLabel: 'EditableText.bringSelectionIntoView'); hideToolbar(); } clipboardStatus.update(); } bool get _allowPaste { return !widget.readOnly && textEditingValue.selection.isValid; } /// Paste text from [Clipboard]. @override Future pasteText(SelectionChangedCause cause) async { if (!_allowPaste) { return; } // Snapshot the input before using `await`. // See https://github.com/flutter/flutter/issues/11427 final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data == null) { return; } _pasteText(cause, data.text!); } void _pasteText(SelectionChangedCause cause, String text) { if (!_allowPaste) { return; } // After the paste, the cursor should be collapsed and located after the // pasted content. final TextSelection selection = textEditingValue.selection; final int lastSelectionIndex = math.max( selection.baseOffset, selection.extentOffset, ); // bggRGjQaUbCoE _pasteText widget.controller.syncRichText( selection.isCollapsed ? TextEditingDeltaInsertion( oldText: textEditingValue.text, textInserted: text, insertionOffset: selection.baseOffset, selection: TextSelection.collapsed(offset: lastSelectionIndex), composing: TextRange.empty, ) : TextEditingDeltaReplacement( oldText: textEditingValue.text, replacementText: text, replacedRange: selection, selection: TextSelection.collapsed(offset: lastSelectionIndex), composing: TextRange.empty, ), ); final newValue = _value.copyWith( text: widget.controller.plainText, selection: widget.controller.newSelection, composing: TextRange.empty, ); userUpdateTextEditingValue(newValue, cause); if (cause == SelectionChangedCause.toolbar) { // Schedule a call to bringIntoView() after renderEditable updates. SchedulerBinding.instance.addPostFrameCallback((_) { if (mounted) { bringIntoView(textEditingValue.selection.extent); } }, debugLabel: 'EditableText.bringSelectionIntoView'); hideToolbar(); } } /// Select the entire text value. @override void selectAll(SelectionChangedCause cause) { if (widget.readOnly && widget.obscureText) { // If we can't modify it, and we can't copy it, there's no point in // selecting it. return; } userUpdateTextEditingValue( textEditingValue.copyWith( selection: TextSelection( baseOffset: 0, extentOffset: textEditingValue.text.length, ), ), cause, ); if (cause == SelectionChangedCause.toolbar) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: case TargetPlatform.fuchsia: break; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: hideToolbar(); } switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: bringIntoView(textEditingValue.selection.extent); case TargetPlatform.macOS: case TargetPlatform.iOS: break; } } } /// Look up the current selection, /// as in the "Look Up" edit menu button on iOS. /// /// Currently this is only implemented for iOS. /// /// Throws an error if the selection is empty or collapsed. Future lookUpSelection(SelectionChangedCause cause) async { assert(!widget.obscureText); final String text = textEditingValue.selection.textInside( textEditingValue.text, ); if (widget.obscureText || text.isEmpty) { return; } await SystemChannels.platform.invokeMethod('LookUp.invoke', text); } /// Launch a web search on the current selection, /// as in the "Search Web" edit menu button on iOS. /// /// Currently this is only implemented for iOS. /// /// When 'obscureText' is true or the selection is empty, /// this function will not do anything Future searchWebForSelection(SelectionChangedCause cause) async { assert(!widget.obscureText); if (widget.obscureText) { return; } final String text = textEditingValue.selection.textInside( textEditingValue.text, ); if (text.isNotEmpty) { await SystemChannels.platform.invokeMethod('SearchWeb.invoke', text); } } /// Launch the share interface for the current selection, /// as in the "Share..." edit menu button on iOS. /// /// Currently this is only implemented for iOS and Android. /// /// When 'obscureText' is true or the selection is empty, /// this function will not do anything Future shareSelection(SelectionChangedCause cause) async { assert(!widget.obscureText); if (widget.obscureText) { return; } final String text = textEditingValue.selection.textInside( textEditingValue.text, ); if (text.isNotEmpty) { await SystemChannels.platform.invokeMethod('Share.invoke', text); } } void _startLiveTextInput(SelectionChangedCause cause) { if (!liveTextInputEnabled) { return; } if (_hasInputConnection) { LiveText.startLiveTextInput(); } if (cause == SelectionChangedCause.toolbar) { hideToolbar(); } } /// Finds specified [SuggestionSpan] that matches the provided index using /// binary search. /// /// See also: /// /// * [SpellCheckSuggestionsToolbar], the Material style spell check /// suggestions toolbar that uses this method to render the correct /// suggestions in the toolbar for a misspelled word. SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) { if (!_spellCheckResultsReceived || spellCheckResults!.suggestionSpans.last.range.end < cursorIndex) { // No spell check results have been received or the cursor index is out // of range that suggestionSpans covers. return null; } final List suggestionSpans = spellCheckResults!.suggestionSpans; var leftIndex = 0; int rightIndex = suggestionSpans.length - 1; var midIndex = 0; while (leftIndex <= rightIndex) { midIndex = ((leftIndex + rightIndex) / 2).floor(); final int currentSpanStart = suggestionSpans[midIndex].range.start; final int currentSpanEnd = suggestionSpans[midIndex].range.end; if (cursorIndex <= currentSpanEnd && cursorIndex >= currentSpanStart) { return suggestionSpans[midIndex]; } else if (cursorIndex <= currentSpanStart) { rightIndex = midIndex - 1; } else { leftIndex = midIndex + 1; } } return null; } /// Infers the [SpellCheckConfiguration] used to perform spell check. /// /// If spell check is enabled, this will try to infer a value for /// the [SpellCheckService] if left unspecified. static SpellCheckConfiguration _inferSpellCheckConfiguration( SpellCheckConfiguration? configuration, ) { final SpellCheckService? spellCheckService = configuration?.spellCheckService; final bool spellCheckAutomaticallyDisabled = configuration == null || configuration == const SpellCheckConfiguration.disabled(); final bool spellCheckServiceIsConfigured = spellCheckService != null || WidgetsBinding .instance .platformDispatcher .nativeSpellCheckServiceDefined; if (spellCheckAutomaticallyDisabled || !spellCheckServiceIsConfigured) { // Only enable spell check if a non-disabled configuration is provided // and if that configuration does not specify a spell check service, // a native spell checker must be supported. assert(() { if (!spellCheckAutomaticallyDisabled && !spellCheckServiceIsConfigured) { FlutterError.reportError( FlutterErrorDetails( exception: FlutterError( 'Spell check was enabled with spellCheckConfiguration, but the ' 'current platform does not have a supported spell check ' 'service, and none was provided. Consider disabling spell ' 'check for this platform or passing a SpellCheckConfiguration ' 'with a specified spell check service.', ), library: 'widget library', stack: StackTrace.current, ), ); } return true; }()); return const SpellCheckConfiguration.disabled(); } return configuration.copyWith( spellCheckService: spellCheckService ?? DefaultSpellCheckService(), ); } /// Returns the [ContextMenuButtonItem]s for the given [ToolbarOptions]. @Deprecated( 'Use `contextMenuBuilder` instead of `toolbarOptions`. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) List? buttonItemsForToolbarOptions([ TargetPlatform? targetPlatform, ]) { final ToolbarOptions toolbarOptions = widget.toolbarOptions; if (toolbarOptions == ToolbarOptions.empty) { return null; } return [ if (toolbarOptions.cut && cutEnabled) ContextMenuButtonItem( onPressed: () { cutSelection(SelectionChangedCause.toolbar); }, type: ContextMenuButtonType.cut, ), if (toolbarOptions.copy && copyEnabled) ContextMenuButtonItem( onPressed: () { copySelection(SelectionChangedCause.toolbar); }, type: ContextMenuButtonType.copy, ), if (toolbarOptions.paste && pasteEnabled) ContextMenuButtonItem( onPressed: () { pasteText(SelectionChangedCause.toolbar); }, type: ContextMenuButtonType.paste, ), if (toolbarOptions.selectAll && selectAllEnabled) ContextMenuButtonItem( onPressed: () { selectAll(SelectionChangedCause.toolbar); }, type: ContextMenuButtonType.selectAll, ), ]; } /// Gets the line heights at the start and end of the selection for the given /// [EditableTextState]. /// /// See also: /// /// * [TextSelectionToolbarAnchors.getSelectionRect], which depends on this /// information. ({double startGlyphHeight, double endGlyphHeight}) getGlyphHeights() { final TextSelection selection = textEditingValue.selection; // Only calculate handle rects if the text in the previous frame // is the same as the text in the current frame. This is done because // widget.renderObject contains the renderEditable from the previous frame. // If the text changed between the current and previous frames then // widget.renderObject.getRectForComposingRange might fail. In cases where // the current frame is different from the previous we fall back to // renderObject.preferredLineHeight. final InlineSpan span = renderEditable.text!; final String prevText = span.toPlainText(); final String currText = textEditingValue.text; if (prevText != currText || !selection.isValid || selection.isCollapsed) { return ( startGlyphHeight: renderEditable.preferredLineHeight, endGlyphHeight: renderEditable.preferredLineHeight, ); } final String selectedGraphemes = selection.textInside(currText); final int firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length; final Rect? startCharacterRect = renderEditable.getRectForComposingRange( TextRange( start: selection.start, end: selection.start + firstSelectedGraphemeExtent, ), ); final int lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length; final Rect? endCharacterRect = renderEditable.getRectForComposingRange( TextRange( start: selection.end - lastSelectedGraphemeExtent, end: selection.end, ), ); return ( startGlyphHeight: startCharacterRect?.height ?? renderEditable.preferredLineHeight, endGlyphHeight: endCharacterRect?.height ?? renderEditable.preferredLineHeight, ); } /// {@template flutter.widgets.EditableText.getAnchors} /// Returns the anchor points for the default context menu. /// {@endtemplate} /// /// See also: /// /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s /// for the default context menu buttons. TextSelectionToolbarAnchors get contextMenuAnchors { if (renderEditable.lastSecondaryTapDownPosition != null) { return TextSelectionToolbarAnchors( primaryAnchor: renderEditable.lastSecondaryTapDownPosition!, ); } final ( startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight, ) = getGlyphHeights(); final TextSelection selection = textEditingValue.selection; final List points = renderEditable .getEndpointsForSelection(selection); return TextSelectionToolbarAnchors.fromSelection( renderBox: renderEditable, startGlyphHeight: startGlyphHeight, endGlyphHeight: endGlyphHeight, selectionEndpoints: points, ); } /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu for [EditableText]. /// /// See also: /// /// * [EditableText.getEditableButtonItems], which performs a similar role, /// but for any editable field, not just specifically EditableText. /// * [SystemContextMenu.getDefaultItems], which performs a similar role, but /// for the system-rendered context menu. /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar /// role but for content that is selectable but not editable. /// * [contextMenuAnchors], which provides the anchor points for the default /// context menu. /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can /// take a list of [ContextMenuButtonItem]s with /// [AdaptiveTextSelectionToolbar.buttonItems]. /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the /// button Widgets for the current platform given [ContextMenuButtonItem]s. List get contextMenuButtonItems { return buttonItemsForToolbarOptions() ?? EditableText.getEditableButtonItems( clipboardStatus: clipboardStatus.value, onCopy: copyEnabled ? () => copySelection(SelectionChangedCause.toolbar) : null, onCut: cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null, onPaste: pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null, onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, onLookUp: lookUpEnabled ? () => lookUpSelection(SelectionChangedCause.toolbar) : null, onSearchWeb: searchWebEnabled ? () => searchWebForSelection(SelectionChangedCause.toolbar) : null, onShare: shareEnabled ? () => shareSelection(SelectionChangedCause.toolbar) : null, onLiveTextInput: liveTextInputEnabled ? () => _startLiveTextInput(SelectionChangedCause.toolbar) : null, ) ..addAll(_textProcessingActionButtonItems); } List get _textProcessingActionButtonItems { final buttonItems = []; final TextSelection selection = textEditingValue.selection; if (widget.obscureText || !selection.isValid || selection.isCollapsed) { return buttonItems; } for (final ProcessTextAction action in _processTextActions) { buttonItems.add( ContextMenuButtonItem( label: action.label, onPressed: () async { final String selectedText = selection.textInside( textEditingValue.text, ); if (selectedText.isNotEmpty) { final String? processedText = await _processTextService .processTextAction( action.id, selectedText, widget.readOnly, ); // If an activity does not return a modified version, just hide the toolbar. // Otherwise use the result to replace the selected text. if (processedText != null && _allowPaste) { _pasteText(SelectionChangedCause.toolbar, processedText); } else { hideToolbar(); } } }, ), ); } return buttonItems; } // State lifecycle: @protected @override void initState() { super.initState(); _liveTextInputStatus?.addListener(_onChangedLiveTextInputStatus); clipboardStatus.addListener(_onChangedClipboardStatus); widget.controller.addListener(_didChangeTextEditingValue); widget.focusNode.addListener(_handleFocusChanged); _cursorVisibilityNotifier.value = widget.showCursor; _spellCheckConfiguration = _inferSpellCheckConfiguration( widget.spellCheckConfiguration, ); _appLifecycleListener = AppLifecycleListener(onResume: _onResume); _initProcessTextActions(); } void _onResume() { _justResumed = true; // To prevent adding multiple listeners, remove any existing one first. FocusManager.instance.removeListener(_resetJustResumed); // Reset _justResumed as soon as there is a focus change. FocusManager.instance.addListener(_resetJustResumed); } void _resetJustResumed() { _justResumed = false; FocusManager.instance.removeListener(_resetJustResumed); } /// Query the engine to initialize the list of text processing actions to show /// in the text selection toolbar. Future _initProcessTextActions() async { _processTextActions ..clear() ..addAll(await _processTextService.queryTextActions()); } // Whether `TickerMode.of(context)` is true and animations (like blinking the // cursor) are supposed to run. bool _tickersEnabled = true; @protected @override void didChangeDependencies() { super.didChangeDependencies(); _style = MediaQuery.boldTextOf(context) ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) : widget.style; final AutofillGroupState? newAutofillGroup = AutofillGroup.maybeOf(context); if (currentAutofillScope != newAutofillGroup) { _currentAutofillScope?.unregister(autofillId); _currentAutofillScope = newAutofillGroup; _currentAutofillScope?.register(_effectiveAutofillClient); } if (!_didAutoFocus && widget.autofocus) { _didAutoFocus = true; SchedulerBinding.instance.addPostFrameCallback((_) { if (mounted && renderEditable.hasSize) { _flagInternalFocus(); FocusScope.of(context).autofocus(widget.focusNode); } }, debugLabel: 'EditableText.autofocus'); } // Restart or stop the blinking cursor when TickerMode changes. final bool newTickerEnabled = TickerMode.of(context); if (_tickersEnabled != newTickerEnabled) { _tickersEnabled = newTickerEnabled; if (_showBlinkingCursor) { _startCursorBlink(); } else if (!_tickersEnabled && _cursorTimer != null) { _stopCursorBlink(); } } // Check for changes in viewId. if (_hasInputConnection) { final int newViewId = View.of(context).viewId; if (newViewId != _viewId) { _textInputConnection!.updateConfig( _effectiveAutofillClient.textInputConfiguration, ); } } if (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.android) { return; } // Hide the text selection toolbar on mobile when orientation changes. final Orientation orientation = MediaQuery.orientationOf(context); if (_lastOrientation == null) { _lastOrientation = orientation; return; } if (orientation != _lastOrientation) { _lastOrientation = orientation; if (defaultTargetPlatform == TargetPlatform.iOS) { hideToolbar(false); } if (defaultTargetPlatform == TargetPlatform.android) { hideToolbar(); } } if (_listeningToScrollNotificationObserver) { // Only update subscription when we have previously subscribed to the // scroll notification observer. We only subscribe to the scroll // notification observer when the context menu is shown on platforms that // support _platformSupportsFadeOnScroll. _scrollNotificationObserver?.removeListener( _handleContextMenuOnParentScroll, ); _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); _scrollNotificationObserver?.addListener( _handleContextMenuOnParentScroll, ); } } @protected @override void didUpdateWidget(EditableText oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { oldWidget.controller.removeListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue); _updateRemoteEditingValueIfNeeded(); } if (_selectionOverlay != null && (widget.contextMenuBuilder != oldWidget.contextMenuBuilder || widget.selectionControls != oldWidget.selectionControls || widget.onSelectionHandleTapped != oldWidget.onSelectionHandleTapped || widget.dragStartBehavior != oldWidget.dragStartBehavior || widget.magnifierConfiguration != oldWidget.magnifierConfiguration)) { final bool shouldShowToolbar = _selectionOverlay!.toolbarIsVisible; final bool shouldShowHandles = _selectionOverlay!.handlesVisible; _selectionOverlay!.dispose(); _selectionOverlay = _createSelectionOverlay(); if (shouldShowToolbar || shouldShowHandles) { SchedulerBinding.instance.addPostFrameCallback((Duration _) { if (shouldShowToolbar) { _selectionOverlay!.showToolbar(); } if (shouldShowHandles) { _selectionOverlay!.showHandles(); } }); } } else if (widget.controller.selection != oldWidget.controller.selection) { _selectionOverlay?.update(_value); } _selectionOverlay?.handlesVisible = widget.showSelectionHandles; if (widget.autofillClient != oldWidget.autofillClient) { _currentAutofillScope?.unregister( oldWidget.autofillClient?.autofillId ?? autofillId, ); _currentAutofillScope?.register(_effectiveAutofillClient); } if (widget.focusNode != oldWidget.focusNode) { oldWidget.focusNode.removeListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged); updateKeepAlive(); } if (!_shouldCreateInputConnection) { _closeInputConnectionIfNeeded(); } else if (oldWidget.readOnly && _hasFocus) { // _openInputConnection must be called after layout information is available. // See https://github.com/flutter/flutter/issues/126312 SchedulerBinding.instance.addPostFrameCallback((Duration _) { _openInputConnection(); }, debugLabel: 'EditableText.openInputConnection'); } if (kIsWeb && _hasInputConnection) { if (oldWidget.readOnly != widget.readOnly) { _textInputConnection!.updateConfig( _effectiveAutofillClient.textInputConfiguration, ); } } if (_hasInputConnection) { if (oldWidget.obscureText != widget.obscureText || oldWidget.keyboardType != widget.keyboardType) { _textInputConnection!.updateConfig( _effectiveAutofillClient.textInputConfiguration, ); } } if (widget.style != oldWidget.style) { // The _textInputConnection will pick up the new style when it attaches in // _openInputConnection. _style = MediaQuery.boldTextOf(context) ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) : widget.style; if (_hasInputConnection) { _textInputConnection!.setStyle( fontFamily: _style.fontFamily, fontSize: _style.fontSize, fontWeight: _style.fontWeight, textDirection: _textDirection, textAlign: widget.textAlign, ); } } if (widget.showCursor != oldWidget.showCursor) { _startOrStopCursorTimerIfNeeded(); } final bool canPaste = widget.selectionControls is TextSelectionHandleControls ? pasteEnabled : widget.selectionControls?.canPaste(this) ?? false; if (widget.selectionEnabled && pasteEnabled && canPaste) { clipboardStatus.update(); } } void _disposeScrollNotificationObserver() { _listeningToScrollNotificationObserver = false; if (_scrollNotificationObserver != null) { _scrollNotificationObserver!.removeListener( _handleContextMenuOnParentScroll, ); _scrollNotificationObserver = null; } } @protected @override void dispose() { _internalScrollController?.dispose(); _currentAutofillScope?.unregister(autofillId); widget.controller.removeListener(_didChangeTextEditingValue); _floatingCursorResetController?.dispose(); _floatingCursorResetController = null; _closeInputConnectionIfNeeded(); assert(!_hasInputConnection); _cursorTimer?.cancel(); _cursorTimer = null; _backingCursorBlinkOpacityController?.dispose(); _backingCursorBlinkOpacityController = null; _selectionOverlay?.dispose(); _selectionOverlay = null; widget.focusNode.removeListener(_handleFocusChanged); WidgetsBinding.instance.removeObserver(this); _liveTextInputStatus?.removeListener(_onChangedLiveTextInputStatus); _liveTextInputStatus?.dispose(); clipboardStatus ..removeListener(_onChangedClipboardStatus) ..dispose(); _cursorVisibilityNotifier.dispose(); _appLifecycleListener.dispose(); FocusManager.instance.removeListener(_unflagInternalFocus); FocusManager.instance.removeListener(_resetJustResumed); _disposeScrollNotificationObserver(); super.dispose(); assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth'); } // TextInputClient implementation: /// The last known [TextEditingValue] of the platform text input plugin. /// /// This value is updated when the platform text input plugin sends a new /// update via [updateEditingValue], or when [EditableText] calls /// [TextInputConnection.setEditingState] to overwrite the platform text input /// plugin's [TextEditingValue]. /// /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the /// remote value is outdated and needs updating. TextEditingValue? _lastKnownRemoteTextEditingValue; @override TextEditingValue get currentTextEditingValue => _value; @override void updateEditingValue( TextEditingValue value, { TextEditingValue? remoteValue, }) { // This method handles text editing state updates from the platform text // input plugin. The [EditableText] may not have the focus or an open input // connection, as autofill can update a disconnected [EditableText]. // Since we still have to support keyboard select, this is the best place // to disable text updating. if (!_shouldCreateInputConnection) { return; } if (_checkNeedsAdjustAffinity(value)) { value = value.copyWith( selection: value.selection.copyWith( affinity: _value.selection.affinity, ), ); } if (widget.readOnly) { // In the read-only case, we only care about selection changes, and reject // everything else. value = _value.copyWith(selection: value.selection); } _lastKnownRemoteTextEditingValue = remoteValue ?? value; if (value == _value) { if (remoteValue != null && Platform.isIOS) { _updateRemoteEditingValueIfNeeded(); } // This is possible, for example, when the numeric keyboard is input, // the engine will notify twice for the same value. // Track at https://github.com/flutter/flutter/issues/65811 return; } if (value.text == _value.text && value.composing == _value.composing) { // `selection` is the only change. SelectionChangedCause cause; if (_textInputConnection?.scribbleInProgress ?? false) { cause = SelectionChangedCause.stylusHandwriting; } else if (_pointOffsetOrigin != null) { // For floating cursor selection when force pressing the space bar. cause = SelectionChangedCause.forcePress; } else { cause = SelectionChangedCause.keyboard; } _handleSelectionChanged(value.selection, cause); } else { if (value.text != _value.text) { // Hide the toolbar if the text was changed, but only hide the toolbar // overlay; the selection handle's visibility will be handled // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673 hideToolbar(false); } _currentPromptRectRange = null; final bool revealObscuredInput = _hasInputConnection && widget.obscureText && WidgetsBinding.instance.platformDispatcher.brieflyShowPassword && value.text.length == _value.text.length + 1; _obscureShowCharTicksPending = revealObscuredInput ? _kObscureShowLatestCharCursorTicks : 0; _obscureLatestCharIndex = revealObscuredInput ? _value.selection.baseOffset : null; _formatAndSetValue(value, SelectionChangedCause.keyboard); } if (_showBlinkingCursor && _cursorTimer != null) { // To keep the cursor from blinking while typing, restart the timer here. _stopCursorBlink(resetCharTicks: false); _startCursorBlink(); } // Wherever the value is changed by the user, schedule a showCaretOnScreen // to make sure the user can see the changes they just made. Programmatic // changes to `textEditingValue` do not trigger the behavior even if the // text field is focused. scheduleShowCaretOnScreen(withAnimation: true); } bool _checkNeedsAdjustAffinity(TextEditingValue value) { // Trust the engine affinity if the text changes or selection changes. return value.text == _value.text && value.selection.isCollapsed == _value.selection.isCollapsed && value.selection.start == _value.selection.start && value.selection.affinity != _value.selection.affinity; } @override void performAction(TextInputAction action) { switch (action) { case TextInputAction.newline: // If this is a multiline EditableText, do nothing for a "newline" // action; The newline is already inserted. Otherwise, finalize // editing. if (!_isMultiline) { _finalizeEditing(action, shouldUnfocus: true); } else if (HardwareKeyboard.instance.isControlPressed) { final ctr = widget.controller; final offset = ctr.selection.end; // delete newline ctr.syncRichText( TextEditingDeltaDeletion( composing: TextRange.empty, selection: TextSelection.collapsed(offset: offset - 1), deletedRange: TextRange(start: offset - 1, end: offset), oldText: ctr.text, ), ); _finalizeEditing(action, shouldUnfocus: true); } case TextInputAction.done: case TextInputAction.go: case TextInputAction.next: case TextInputAction.previous: case TextInputAction.search: case TextInputAction.send: _finalizeEditing(action, shouldUnfocus: true); case TextInputAction.continueAction: case TextInputAction.emergencyCall: case TextInputAction.join: case TextInputAction.none: case TextInputAction.route: case TextInputAction.unspecified: // Finalize editing, but don't give up focus because this keyboard // action does not imply the user is done inputting information. _finalizeEditing(action, shouldUnfocus: false); } } @override void updateEditingValueWithDeltas(List textEditingDeltas) { if (textEditingDeltas.isEmpty) { updateEditingValue(_value.copyWith(composing: TextRange.empty)); return; } TextEditingValue remoteValue = _value; for (final TextEditingDelta delta in textEditingDeltas) { widget.controller.syncRichText(delta); remoteValue = delta.apply(remoteValue); } final newValue = _value.copyWith( text: widget.controller.plainText, selection: widget.controller.newSelection, composing: textEditingDeltas.last.composing, ); updateEditingValue(newValue, remoteValue: remoteValue); // TextEditingValue value = _value; // for (final TextEditingDelta delta in textEditingDeltas) { // value = delta.apply(value); // } // updateEditingValue(value); } @override void performPrivateCommand(String action, Map data) { widget.onAppPrivateCommand?.call(action, data); } @override void insertContent(KeyboardInsertedContent content) { assert( widget.contentInsertionConfiguration?.allowedMimeTypes.contains( content.mimeType, ) ?? false, ); widget.contentInsertionConfiguration?.onContentInserted.call(content); } // The original position of the caret on FloatingCursorDragState.start. Offset? _startCaretCenter; // The most recent text position as determined by the location of the floating // cursor. TextPosition? _lastTextPosition; // The offset of the floating cursor as determined from the start call. Offset? _pointOffsetOrigin; // The most recent position of the floating cursor. Offset? _lastBoundedOffset; // Because the center of the cursor is preferredLineHeight / 2 below the touch // origin, but the touch origin is used to determine which line the cursor is // on, we need this offset to correctly render and move the cursor. Offset get _floatingCursorOffset => Offset(0, renderEditable.preferredLineHeight / 2); @override void updateFloatingCursor(RawFloatingCursorPoint point) { _floatingCursorResetController ??= AnimationController(vsync: this) ..addListener(_onFloatingCursorResetTick); switch (point.state) { case FloatingCursorDragState.Start: if (_floatingCursorResetController!.isAnimating) { _floatingCursorResetController!.stop(); _onFloatingCursorResetTick(); } // Stop cursor blinking and making it visible. _stopCursorBlink(resetCharTicks: false); _cursorBlinkOpacityController.value = 1.0; // We want to send in points that are centered around a (0,0) origin, so // we cache the position. _pointOffsetOrigin = point.offset; final Offset startCaretCenter; final TextPosition currentTextPosition; final bool shouldResetOrigin; // Only non-null when starting a floating cursor via long press. if (point.startLocation != null) { shouldResetOrigin = false; (startCaretCenter, currentTextPosition) = point.startLocation!; } else { shouldResetOrigin = true; currentTextPosition = TextPosition( offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity, ); startCaretCenter = renderEditable .getLocalRectForCaret(currentTextPosition) .center; } _startCaretCenter = startCaretCenter; _lastBoundedOffset = renderEditable .calculateBoundedFloatingCursorOffset( _startCaretCenter! - _floatingCursorOffset, shouldResetOrigin: shouldResetOrigin, ); _lastTextPosition = currentTextPosition; renderEditable.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!, ); case FloatingCursorDragState.Update: final Offset centeredPoint = point.offset! - _pointOffsetOrigin!; final Offset rawCursorOffset = _startCaretCenter! + centeredPoint - _floatingCursorOffset; _lastBoundedOffset = renderEditable .calculateBoundedFloatingCursorOffset(rawCursorOffset); _lastTextPosition = renderEditable.getPositionForPoint( renderEditable.localToGlobal( _lastBoundedOffset! + _floatingCursorOffset, ), ); // bggRGjQaUbCoE ios single long press _lastTextPosition = widget.controller.dragOffset(_lastTextPosition!); renderEditable.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!, ); case FloatingCursorDragState.End: // Resume cursor blinking. if (_hasFocus) { _startCursorBlink(); } // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { _floatingCursorResetController!.value = 0.0; _floatingCursorResetController!.animateTo( 1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate, ); } } } void _onFloatingCursorResetTick() { final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset; if (_floatingCursorResetController!.isCompleted) { renderEditable.setFloatingCursor( FloatingCursorDragState.End, finalPosition, _lastTextPosition!, ); // During a floating cursor's move gesture (1 finger), a cursor is // animated only visually, without actually updating the selection. // Only after move gesture is complete, this function will be called // to actually update the selection to the new cursor location with // zero selection length. // However, During a floating cursor's selection gesture (2 fingers), the // selection is constantly updated by the engine throughout the gesture. // Thus when the gesture is complete, we should not update the selection // to the cursor location with zero selection length, because that would // overwrite the selection made by floating cursor selection. // Here we use `isCollapsed` to distinguish between floating cursor's // move gesture (1 finger) vs selection gesture (2 fingers), as // the engine does not provide information other than notifying a // new selection during with selection gesture (2 fingers). if (renderEditable.selection!.isCollapsed) { // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. _handleSelectionChanged( TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress, ); } _startCaretCenter = null; _lastTextPosition = null; _pointOffsetOrigin = null; _lastBoundedOffset = null; } else { final double lerpValue = _floatingCursorResetController!.value; final double lerpX = ui.lerpDouble( _lastBoundedOffset!.dx, finalPosition.dx, lerpValue, )!; final double lerpY = ui.lerpDouble( _lastBoundedOffset!.dy, finalPosition.dy, lerpValue, )!; renderEditable.setFloatingCursor( FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue, ); } } @pragma('vm:notify-debugger-on-exception') void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) { // Take any actions necessary now that the user has completed editing. if (widget.onEditingComplete != null) { try { widget.onEditingComplete!(); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets', context: ErrorDescription( 'while calling onEditingComplete for $action', ), ), ); } } else { // Default behavior if the developer did not provide an // onEditingComplete callback: Finalize editing and remove focus, or move // it to the next/previous field, depending on the action. widget.controller.clearComposing(); if (shouldUnfocus) { switch (action) { case TextInputAction.none: case TextInputAction.unspecified: case TextInputAction.done: case TextInputAction.go: case TextInputAction.search: case TextInputAction.send: case TextInputAction.continueAction: case TextInputAction.join: case TextInputAction.route: case TextInputAction.emergencyCall: case TextInputAction.newline: widget.focusNode.unfocus(); case TextInputAction.next: widget.focusNode.nextFocus(); case TextInputAction.previous: widget.focusNode.previousFocus(); } } } final ValueChanged? onSubmitted = widget.onSubmitted; if (onSubmitted == null) { return; } // Invoke optional callback with the user's submitted content. try { onSubmitted(_value.text); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets', context: ErrorDescription('while calling onSubmitted for $action'), ), ); } // If `shouldUnfocus` is true, the text field should no longer be focused // after the microtask queue is drained. But in case the developer cancelled // the focus change in the `onSubmitted` callback by focusing this input // field again, reset the soft keyboard. // See https://github.com/flutter/flutter/issues/84240. // // `_restartConnectionIfNeeded` creates a new TextInputConnection to replace // the current one. This on iOS switches to a new input view and on Android // restarts the input method, and in both cases the soft keyboard will be // reset. if (shouldUnfocus) { _scheduleRestartConnection(); } } int _batchEditDepth = 0; /// Begins a new batch edit, within which new updates made to the text editing /// value will not be sent to the platform text input plugin. /// /// Batch edits nest. When the outermost batch edit finishes, [endBatchEdit] /// will attempt to send [currentTextEditingValue] to the text input plugin if /// it detected a change. void beginBatchEdit() { _batchEditDepth += 1; } /// Ends the current batch edit started by the last call to [beginBatchEdit], /// and send [currentTextEditingValue] to the text input plugin if needed. /// /// Throws an error in debug mode if this [EditableText] is not in a batch /// edit. void endBatchEdit() { _batchEditDepth -= 1; assert( _batchEditDepth >= 0, 'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.', ); _updateRemoteEditingValueIfNeeded(); } void _updateRemoteEditingValueIfNeeded() { if (_batchEditDepth > 0 || !_hasInputConnection) { return; } final TextEditingValue localValue = _value; if (localValue == _lastKnownRemoteTextEditingValue) { return; } _textInputConnection!.setEditingState(localValue); _lastKnownRemoteTextEditingValue = localValue; } TextEditingValue get _value => widget.controller.value; set _value(TextEditingValue value) { widget.controller.value = value; } bool get _hasFocus => widget.focusNode.hasFocus; bool get _isMultiline => widget.maxLines != 1; /// Flag to track whether this [EditableText] was in focus when [onTapOutside] /// was called. /// /// This is used to determine whether [onTapUpOutside] should be called. /// The reason [_hasFocus] can't be used directly is because [onTapOutside] /// might unfocus this [EditableText] and block the [onTapUpOutside] call. bool _hadFocusOnTapDown = false; // Finds the closest scroll offset to the current scroll offset that fully // reveals the given caret rect. If the given rect's main axis extent is too // large to be fully revealed in `renderEditable`, it will be centered along // the main axis. // // If this is a multiline EditableText (which means the Editable can only // scroll vertically), the given rect's height will first be extended to match // `renderEditable.preferredLineHeight`, before the target scroll offset is // calculated. RevealedOffset _getOffsetToRevealCaret(Rect rect) { if (!_scrollController.position.allowImplicitScrolling) { return RevealedOffset(offset: _scrollController.offset, rect: rect); } final Size editableSize = renderEditable.size; final double additionalOffset; final Offset unitOffset; if (!_isMultiline) { additionalOffset = rect.width >= editableSize.width // Center `rect` if it's oversized. ? editableSize.width / 2 - rect.center.dx // Valid additional offsets range from (rect.right - size.width) // to (rect.left). Pick the closest one if out of range. : clampDouble(0.0, rect.right - editableSize.width, rect.left); unitOffset = const Offset(1, 0); } else { // The caret is vertically centered within the line. Expand the caret's // height so that it spans the line because we're going to ensure that the // entire expanded caret is scrolled into view. final expandedRect = Rect.fromCenter( center: rect.center, width: rect.width, height: math.max(rect.height, renderEditable.preferredLineHeight), ); additionalOffset = expandedRect.height >= editableSize.height ? editableSize.height / 2 - expandedRect.center.dy : clampDouble( 0.0, expandedRect.bottom - editableSize.height, expandedRect.top, ); unitOffset = const Offset(0, 1); } // No overscrolling when encountering tall fonts/scripts that extend past // the ascent. final double targetOffset = clampDouble( additionalOffset + _scrollController.offset, _scrollController.position.minScrollExtent, _scrollController.position.maxScrollExtent, ); final double offsetDelta = _scrollController.offset - targetOffset; return RevealedOffset( rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset, ); } /// Whether to send the autofill information to the autofill service. True by /// default. bool get _needsAutofill => _effectiveAutofillClient .textInputConfiguration .autofillConfiguration .enabled; // Must be called after layout. // See https://github.com/flutter/flutter/issues/126312 void _openInputConnection() { if (!_shouldCreateInputConnection) { return; } if (!_hasInputConnection) { final TextEditingValue localValue = _value; // When _needsAutofill == true && currentAutofillScope == null, autofill // is allowed but saving the user input from the text field is // discouraged. // // In case the autofillScope changes from a non-null value to null, or // _needsAutofill changes to false from true, the platform needs to be // notified to exclude this field from the autofill context. So we need to // provide the autofillId. _textInputConnection = _needsAutofill && currentAutofillScope != null ? currentAutofillScope!.attach( this, _effectiveAutofillClient.textInputConfiguration, ) : TextInput.attach( this, _effectiveAutofillClient.textInputConfiguration, ); _updateSizeAndTransform(); _schedulePeriodicPostFrameCallbacks(); _textInputConnection! ..setStyle( fontFamily: _style.fontFamily, fontSize: _style.fontSize, fontWeight: _style.fontWeight, textDirection: _textDirection, textAlign: widget.textAlign, ) ..setEditingState(localValue) ..show(); if (_needsAutofill) { // Request autofill AFTER the size and the transform have been sent to // the platform text input plugin. _textInputConnection!.requestAutofill(); } _lastKnownRemoteTextEditingValue = localValue; } else { _textInputConnection!.show(); } } void _closeInputConnectionIfNeeded() { if (_hasInputConnection) { _textInputConnection!.close(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; _scribbleCacheKey = null; removeTextPlaceholder(); } } void _openOrCloseInputConnectionIfNeeded() { if (_hasFocus && widget.focusNode.consumeKeyboardToken()) { _openInputConnection(); } else if (!_hasFocus) { _closeInputConnectionIfNeeded(); widget.controller.clearComposing(); } } bool _restartConnectionScheduled = false; void _scheduleRestartConnection() { if (_restartConnectionScheduled) { return; } _restartConnectionScheduled = true; scheduleMicrotask(_restartConnectionIfNeeded); } // Discards the current [TextInputConnection] and establishes a new one. // // This method is rarely needed. This is currently used to reset the input // type when the "submit" text input action is triggered and the developer // puts the focus back to this input field.. void _restartConnectionIfNeeded() { _restartConnectionScheduled = false; if (!_hasInputConnection || !_shouldCreateInputConnection) { return; } _textInputConnection!.close(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; final AutofillScope? currentAutofillScope = _needsAutofill ? this.currentAutofillScope : null; final TextInputConnection newConnection = currentAutofillScope?.attach(this, textInputConfiguration) ?? TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration); _textInputConnection = newConnection; newConnection ..show() ..setStyle( fontFamily: _style.fontFamily, fontSize: _style.fontSize, fontWeight: _style.fontWeight, textDirection: _textDirection, textAlign: widget.textAlign, ) ..setEditingState(_value); _lastKnownRemoteTextEditingValue = _value; } @override void didChangeInputControl( TextInputControl? oldControl, TextInputControl? newControl, ) { if (_hasFocus && _hasInputConnection) { oldControl?.hide(); newControl?.show(); } } @override void connectionClosed() { if (_hasInputConnection) { _textInputConnection!.connectionClosedReceived(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; widget.focusNode.unfocus(); } } // Indicates that a call to _handleFocusChanged originated within // EditableText, allowing it to distinguish between internal and external // focus changes. bool _nextFocusChangeIsInternal = false; // Sets _nextFocusChangeIsInternal to true only until any subsequent focus // change happens. void _flagInternalFocus() { _nextFocusChangeIsInternal = true; FocusManager.instance.addListener(_unflagInternalFocus); } void _unflagInternalFocus() { _nextFocusChangeIsInternal = false; FocusManager.instance.removeListener(_unflagInternalFocus); } /// Express interest in interacting with the keyboard. /// /// If this control is already attached to the keyboard, this function will /// request that the keyboard become visible. Otherwise, this function will /// ask the focus system that it become focused. If successful in acquiring /// focus, the control will then attach to the keyboard and request that the /// keyboard become visible. void requestKeyboard() { if (_hasFocus) { _openInputConnection(); } else { _flagInternalFocus(); widget.focusNode .requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged. } } void _updateOrDisposeSelectionOverlayIfNeeded() { if (_selectionOverlay != null) { if (_hasFocus) { _selectionOverlay!.update(_value); } else { _selectionOverlay!.dispose(); _selectionOverlay = null; } } } final bool _platformSupportsFadeOnScroll = switch (defaultTargetPlatform) { TargetPlatform.android || TargetPlatform.iOS => true, TargetPlatform.fuchsia || TargetPlatform.linux || TargetPlatform.macOS || TargetPlatform.windows => false, }; bool _isInternalScrollableNotification(BuildContext? notificationContext) { final ScrollableState? scrollableState = notificationContext ?.findAncestorStateOfType(); return _scrollableKey.currentContext == scrollableState?.context; } bool _scrollableNotificationIsFromSameSubtree( BuildContext? notificationContext, ) { if (notificationContext == null) { return false; } BuildContext? currentContext = context; // The notification context of a ScrollNotification points to the RawGestureDetector // of the Scrollable. We get the ScrollableState associated with this notification // by looking up the tree. final ScrollableState? notificationScrollableState = notificationContext .findAncestorStateOfType(); if (notificationScrollableState == null) { return false; } while (currentContext != null) { final ScrollableState? scrollableState = currentContext .findAncestorStateOfType(); if (scrollableState == notificationScrollableState) { return true; } currentContext = scrollableState?.context; } return false; } void _handleContextMenuOnParentScroll(ScrollNotification notification) { // Do some preliminary checks to avoid expensive subtree traversal. if (notification is! ScrollStartNotification && notification is! ScrollEndNotification) { return; } switch (notification) { case ScrollStartNotification() when _dataWhenToolbarShowScheduled != null: case ScrollEndNotification() when _dataWhenToolbarShowScheduled == null: break; case ScrollEndNotification() when _dataWhenToolbarShowScheduled!.value != _value: _dataWhenToolbarShowScheduled = null; _disposeScrollNotificationObserver(); case ScrollNotification(:final BuildContext? context) when !_isInternalScrollableNotification(context) && _scrollableNotificationIsFromSameSubtree(context): _handleContextMenuOnScroll(notification); } } Rect _calculateDeviceRect() { final Size screenSize = MediaQuery.sizeOf(context); final ui.FlutterView view = View.of(context); final double obscuredVertical = (view.padding.top + view.padding.bottom + view.viewInsets.bottom) / view.devicePixelRatio; final double obscuredHorizontal = (view.padding.left + view.padding.right) / view.devicePixelRatio; final visibleScreenSize = Size( screenSize.width - obscuredHorizontal, screenSize.height - obscuredVertical, ); return Rect.fromLTWH( view.padding.left / view.devicePixelRatio, view.padding.top / view.devicePixelRatio, visibleScreenSize.width, visibleScreenSize.height, ); } bool _showToolbarOnScreenScheduled = false; void _handleContextMenuOnScroll(ScrollNotification notification) { if (_webContextMenuEnabled) { return; } if (!_platformSupportsFadeOnScroll) { _selectionOverlay?.updateForScroll(); return; } // When the scroll begins and the toolbar is visible, hide it // until scrolling ends. // // The selection and renderEditable need to be visible within the current // viewport for the toolbar to show when scrolling ends. If they are not // then the toolbar is shown when they are scrolled back into view, unless // invalidated by a change in TextEditingValue. if (notification is ScrollStartNotification) { if (_dataWhenToolbarShowScheduled != null) { return; } final bool toolbarIsVisible = _selectionOverlay != null && _selectionOverlay!.toolbarIsVisible && !_selectionOverlay!.spellCheckToolbarIsVisible; if (!toolbarIsVisible) { return; } final List selectionBoxes = renderEditable.getBoxesForSelection( _value.selection, ); final Rect selectionBounds = _value.selection.isCollapsed || selectionBoxes.isEmpty ? renderEditable.getLocalRectForCaret(_value.selection.extent) : selectionBoxes .map((TextBox box) => box.toRect()) .reduce( (Rect result, Rect rect) => result.expandToInclude(rect), ); _dataWhenToolbarShowScheduled = ( value: _value, selectionBounds: selectionBounds, ); _selectionOverlay?.hideToolbar(); } else if (notification is ScrollEndNotification) { if (_dataWhenToolbarShowScheduled == null) { return; } if (_dataWhenToolbarShowScheduled!.value != _value) { // Value has changed so we should invalidate any toolbar scheduling. _dataWhenToolbarShowScheduled = null; _disposeScrollNotificationObserver(); return; } if (_showToolbarOnScreenScheduled) { return; } _showToolbarOnScreenScheduled = true; SchedulerBinding.instance.addPostFrameCallback((Duration _) { _showToolbarOnScreenScheduled = false; if (!mounted || _dataWhenToolbarShowScheduled == null) { return; } if (_dataWhenToolbarShowScheduled!.value != _value) { // Value has changed so we should invalidate any toolbar scheduling. _dataWhenToolbarShowScheduled = null; _disposeScrollNotificationObserver(); return; } final Rect deviceRect = _calculateDeviceRect(); final bool selectionVisibleInEditable = renderEditable.selectionStartInViewport.value || renderEditable.selectionEndInViewport.value; final Rect selectionBounds = MatrixUtils.transformRect( renderEditable.getTransformTo(null), _dataWhenToolbarShowScheduled!.selectionBounds, ); final bool selectionOverlapsWithDeviceRect = !selectionBounds.hasNaN && deviceRect.overlaps(selectionBounds); if (selectionVisibleInEditable && selectionOverlapsWithDeviceRect && _selectionInViewport( _dataWhenToolbarShowScheduled!.selectionBounds, )) { showToolbar(); _dataWhenToolbarShowScheduled = null; } }, debugLabel: 'EditableText.scheduleToolbar'); } } bool _selectionInViewport(Rect selectionBounds) { RenderAbstractViewport? closestViewport = RenderAbstractViewport.maybeOf( renderEditable, ); while (closestViewport != null) { final Rect selectionBoundsLocalToViewport = MatrixUtils.transformRect( renderEditable.getTransformTo(closestViewport), selectionBounds, ); if (selectionBoundsLocalToViewport.hasNaN || closestViewport.paintBounds.hasNaN || !closestViewport.paintBounds.overlaps( selectionBoundsLocalToViewport, )) { return false; } closestViewport = RenderAbstractViewport.maybeOf(closestViewport.parent); } return true; } TextSelectionOverlay _createSelectionOverlay() { final EditableTextContextMenuBuilder? contextMenuBuilder = widget.contextMenuBuilder; final selectionOverlay = TextSelectionOverlay( controller: widget.controller, clipboardStatus: clipboardStatus, context: context, value: _value, debugRequiredFor: widget, toolbarLayerLink: _toolbarLayerLink, startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, renderObject: renderEditable, selectionControls: widget.selectionControls, selectionDelegate: this, dragStartBehavior: widget.dragStartBehavior, onSelectionHandleTapped: widget.onSelectionHandleTapped, contextMenuBuilder: contextMenuBuilder == null || _webContextMenuEnabled ? null : (BuildContext context) { return contextMenuBuilder(context, this); }, magnifierConfiguration: widget.magnifierConfiguration, ); return selectionOverlay; } @pragma('vm:notify-debugger-on-exception') void _handleSelectionChanged( TextSelection selection, SelectionChangedCause? cause, ) { // We return early if the selection is not valid. This can happen when the // text of [EditableText] is updated at the same time as the selection is // changed by a gesture event. final String text = widget.controller.value.text; if (text.length < selection.end || text.length < selection.start) { return; } widget.controller.selection = selection; // This will show the keyboard for all selection changes on the // EditableText except for those triggered by a keyboard input. // Typically EditableText shouldn't take user keyboard input if // it's not focused already. If the EditableText is being // autofilled it shouldn't request focus. switch (cause) { case null: case SelectionChangedCause.doubleTap: case SelectionChangedCause.drag: case SelectionChangedCause.forcePress: case SelectionChangedCause.longPress: case SelectionChangedCause.stylusHandwriting: case SelectionChangedCause.tap: case SelectionChangedCause.toolbar: requestKeyboard(); case SelectionChangedCause.keyboard: } if (widget.selectionControls == null && widget.contextMenuBuilder == null) { _selectionOverlay?.dispose(); _selectionOverlay = null; } else { if (_selectionOverlay == null) { _selectionOverlay = _createSelectionOverlay(); } else { _selectionOverlay!.update(_value); } _selectionOverlay!.handlesVisible = widget.showSelectionHandles; _selectionOverlay!.showHandles(); } // TODO(chunhtai): we should make sure selection actually changed before // we call the onSelectionChanged. // https://github.com/flutter/flutter/issues/76349. try { widget.onSelectionChanged?.call(selection, cause); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets', context: ErrorDescription( 'while calling onSelectionChanged for $cause', ), ), ); } // To keep the cursor from blinking while it moves, restart the timer here. if (_showBlinkingCursor && _cursorTimer != null) { _stopCursorBlink(resetCharTicks: false); _startCursorBlink(); } } // Animation configuration for scrolling the caret back on screen. static const Duration _caretAnimationDuration = Duration(milliseconds: 100); static const Curve _caretAnimationCurve = Curves.fastOutSlowIn; bool _showCaretOnScreenScheduled = false; void scheduleShowCaretOnScreen({required bool withAnimation}) { if (_showCaretOnScreenScheduled) { return; } _showCaretOnScreenScheduled = true; SchedulerBinding.instance.addPostFrameCallback((Duration _) { _showCaretOnScreenScheduled = false; // Since we are in a post frame callback, check currentContext in case // RenderEditable has been disposed (in which case it will be null). final renderEditable = _editableKey.currentContext?.findRenderObject() as RenderEditable?; if (renderEditable == null || !(renderEditable.selection?.isValid ?? false) || !_scrollController.hasClients) { return; } final double lineHeight = renderEditable.preferredLineHeight; // Enlarge the target rect by scrollPadding to ensure that caret is not // positioned directly at the edge after scrolling. double bottomSpacing = widget.scrollPadding.bottom; if (_selectionOverlay?.selectionControls != null) { final double handleHeight = _selectionOverlay!.selectionControls! .getHandleSize(lineHeight) .height; final double interactiveHandleHeight = math.max( handleHeight, kMinInteractiveDimension, ); final Offset anchor = _selectionOverlay!.selectionControls! .getHandleAnchor( TextSelectionHandleType.collapsed, lineHeight, ); final double handleCenter = handleHeight / 2 - anchor.dy; bottomSpacing = math.max( handleCenter + interactiveHandleHeight / 2, bottomSpacing, ); } final EdgeInsets caretPadding = widget.scrollPadding.copyWith( bottom: bottomSpacing, ); final Rect caretRect = renderEditable.getLocalRectForCaret( renderEditable.selection!.extent, ); final RevealedOffset targetOffset = _getOffsetToRevealCaret(caretRect); final Rect rectToReveal; final TextSelection selection = textEditingValue.selection; if (selection.isCollapsed) { rectToReveal = targetOffset.rect; } else { final List selectionBoxes = renderEditable .getBoxesForSelection(selection); // selectionBoxes may be empty if, for example, the selection does not // encompass a full character, like if it only contained part of an // extended grapheme cluster. if (selectionBoxes.isEmpty) { rectToReveal = targetOffset.rect; } else { rectToReveal = selection.baseOffset < selection.extentOffset ? selectionBoxes.last.toRect() : selectionBoxes.first.toRect(); } } if (withAnimation) { _scrollController.animateTo( targetOffset.offset, duration: _caretAnimationDuration, curve: _caretAnimationCurve, ); renderEditable.showOnScreen( rect: caretPadding.inflateRect(rectToReveal), duration: _caretAnimationDuration, curve: _caretAnimationCurve, ); } else { _scrollController.jumpTo(targetOffset.offset); renderEditable.showOnScreen( rect: caretPadding.inflateRect(rectToReveal), ); } }, debugLabel: 'EditableText.showCaret'); } late double _lastBottomViewInset; @override void didChangeMetrics() { if (!mounted) { return; } final ui.FlutterView view = View.of(context); if (_lastBottomViewInset != view.viewInsets.bottom) { SchedulerBinding.instance.addPostFrameCallback((Duration _) { _selectionOverlay?.updateForScroll(); }, debugLabel: 'EditableText.updateForScroll'); if (_lastBottomViewInset < view.viewInsets.bottom) { // Because the metrics change signal from engine will come here every frame // (on both iOS and Android). So we don't need to show caret with animation. scheduleShowCaretOnScreen(withAnimation: false); } } _lastBottomViewInset = view.viewInsets.bottom; } Future _performSpellCheck(final String text) async { try { final Locale? localeForSpellChecking = widget.locale ?? Localizations.maybeLocaleOf(context); assert( localeForSpellChecking != null, 'Locale must be specified in widget or Localization widget must be in scope', ); final List? suggestions = await _spellCheckConfiguration .spellCheckService! .fetchSpellCheckSuggestions(localeForSpellChecking!, text); if (suggestions == null || !mounted) { // The request to fetch spell check suggestions was canceled due to ongoing request, // or the widget was unmounted. return; } spellCheckResults = SpellCheckResults(text, suggestions); final double? lineHeightScaleFactor = MediaQuery.maybeLineHeightScaleFactorOverrideOf( context, ); final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf( context, ); final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf( context, ); renderEditable.text = _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( lineHeightScaleFactor: lineHeightScaleFactor, letterSpacing: letterSpacing, wordSpacing: wordSpacing, textSpan: buildTextSpan(), ); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets', context: ErrorDescription('while performing spell check'), ), ); } } @pragma('vm:notify-debugger-on-exception') void _formatAndSetValue( TextEditingValue value, SelectionChangedCause? cause, { bool userInteraction = false, }) { final TextEditingValue oldValue = _value; final textChanged = oldValue.text != value.text; final bool textCommitted = !oldValue.composing.isCollapsed && value.composing.isCollapsed; final selectionChanged = oldValue.selection != value.selection; if (textChanged || textCommitted) { // Only apply input formatters if the text has changed (including uncommitted // text in the composing region), or when the user committed the composing // text. // Gboard is very persistent in restoring the composing region. Applying // input formatters on composing-region-only changes (except clearing the // current composing region) is very infinite-loop-prone: the formatters // will keep trying to modify the composing region while Gboard will keep // trying to restore the original composing region. try { value = widget.inputFormatters?.fold( value, (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue), ) ?? value; if (spellCheckEnabled && value.text.isNotEmpty && _value.text != value.text) { _performSpellCheck(value.text); } } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets', context: ErrorDescription('while applying input formatters'), ), ); } } final TextSelection oldTextSelection = textEditingValue.selection; // Put all optional user callback invocations in a batch edit to prevent // sending multiple `TextInput.updateEditingValue` messages. beginBatchEdit(); _value = value; // Changes made by the keyboard can sometimes be "out of band" for listening // components, so always send those events, even if we didn't think it // changed. Also, the user long pressing should always send a selection change // as well. if (selectionChanged || (userInteraction && (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.keyboard))) { _handleSelectionChanged(_value.selection, cause); _bringIntoViewBySelectionState(oldTextSelection, value.selection, cause); } final String currentText = _value.text; if (oldValue.text != currentText) { try { widget.onChanged?.call(currentText); } catch (exception, stack) { FlutterError.reportError( FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets', context: ErrorDescription('while calling onChanged'), ), ); } } endBatchEdit(); } void _bringIntoViewBySelectionState( TextSelection oldSelection, TextSelection newSelection, SelectionChangedCause? cause, ) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.drag) { bringIntoView(newSelection.extent); } case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.fuchsia: case TargetPlatform.android: if (cause == SelectionChangedCause.drag) { if (oldSelection.baseOffset != newSelection.baseOffset) { bringIntoView(newSelection.base); } else if (oldSelection.extentOffset != newSelection.extentOffset) { bringIntoView(newSelection.extent); } } } } void _onCursorColorTick() { final double effectiveOpacity = math.min( widget.cursorColor.alpha / 255.0, _cursorBlinkOpacityController.value, ); renderEditable.cursorColor = widget.cursorColor.withValues( alpha: effectiveOpacity, ); _cursorVisibilityNotifier.value = widget.showCursor && (EditableText.debugDeterministicCursor || _cursorBlinkOpacityController.value > 0); } bool get _showBlinkingCursor => _hasFocus && _value.selection.isCollapsed && widget.showCursor && _tickersEnabled && !renderEditable.floatingCursorOn; /// Whether the blinking cursor is actually visible at this precise moment /// (it's hidden half the time, since it blinks). @visibleForTesting bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0; /// The cursor blink interval (the amount of time the cursor is in the "on" /// state or the "off" state). A complete cursor blink period is twice this /// value (half on, half off). @visibleForTesting Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; /// The current status of the text selection handles. @visibleForTesting TextSelectionOverlay? get selectionOverlay => _selectionOverlay; int _obscureShowCharTicksPending = 0; int? _obscureLatestCharIndex; void _startCursorBlink() { assert( !(_cursorTimer?.isActive ?? false) || !(_backingCursorBlinkOpacityController?.isAnimating ?? false), ); if (!widget.showCursor) { return; } if (!_tickersEnabled) { return; } _cursorTimer?.cancel(); _cursorBlinkOpacityController.value = 1.0; if (EditableText.debugDeterministicCursor) { return; } if (widget.cursorOpacityAnimates) { _cursorBlinkOpacityController .animateWith(_iosBlinkCursorSimulation) .whenComplete(_onCursorTick); } else { _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick(); }); } } void _onCursorTick() { if (_obscureShowCharTicksPending > 0) { _obscureShowCharTicksPending = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword ? _obscureShowCharTicksPending - 1 : 0; if (_obscureShowCharTicksPending == 0) { setState(() {}); } } if (widget.cursorOpacityAnimates) { _cursorTimer?.cancel(); // Schedule this as an async task to avoid blocking tester.pumpAndSettle // indefinitely. _cursorTimer = Timer( Duration.zero, () => _cursorBlinkOpacityController .animateWith(_iosBlinkCursorSimulation) .whenComplete(_onCursorTick), ); } else { if (!(_cursorTimer?.isActive ?? false) && _tickersEnabled) { _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick(); }); } _cursorBlinkOpacityController.value = _cursorBlinkOpacityController.value == 0 ? 1 : 0; } } void _stopCursorBlink({bool resetCharTicks = true}) { // If the cursor is animating, stop the animation, and we always // want the cursor to be visible when the floating cursor is enabled. _cursorBlinkOpacityController.value = renderEditable.floatingCursorOn ? 1.0 : 0.0; _cursorTimer?.cancel(); _cursorTimer = null; if (resetCharTicks) { _obscureShowCharTicksPending = 0; } } void _startOrStopCursorTimerIfNeeded() { if (!_showBlinkingCursor) { _stopCursorBlink(); } else if (_cursorTimer == null) { _startCursorBlink(); } } void _didChangeTextEditingValue() { if (_hasFocus && !_value.selection.isValid) { // If this field is focused and the selection is invalid, place the cursor at // the end. Does not rely on _handleFocusChanged because it makes selection // handles visible on Android. // Unregister as a listener to the text controller while making the change. widget.controller.removeListener(_didChangeTextEditingValue); widget.controller.selection = _adjustedSelectionWhenFocused()!; widget.controller.addListener(_didChangeTextEditingValue); } _updateRemoteEditingValueIfNeeded(); _startOrStopCursorTimerIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded(); // TODO(abarth): Teach RenderEditable about ValueNotifier // to avoid this setState(). setState(() { /* We use widget.controller.value in build(). */ }); _verticalSelectionUpdateAction.stopCurrentVerticalRunIfSelectionChanges(); } void _handleFocusChanged() { _openOrCloseInputConnectionIfNeeded(); _startOrStopCursorTimerIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded(); if (_hasFocus) { // Listen for changing viewInsets, which indicates keyboard showing up. WidgetsBinding.instance.addObserver(this); _lastBottomViewInset = View.of(context).viewInsets.bottom; if (!widget.readOnly) { scheduleShowCaretOnScreen(withAnimation: true); } final TextSelection? updatedSelection = _adjustedSelectionWhenFocused(); if (updatedSelection != null) { _handleSelectionChanged(updatedSelection, null); } } else { WidgetsBinding.instance.removeObserver(this); setState(() { _currentPromptRectRange = null; }); } updateKeepAlive(); } TextSelection? _adjustedSelectionWhenFocused() { TextSelection? selection; final bool shouldSelectAll = widget.selectAllOnFocus && widget.selectionEnabled && !_isMultiline && !_nextFocusChangeIsInternal && !_justResumed; _justResumed = false; if (shouldSelectAll) { // On native web and desktop platforms, single line tags // select all when receiving focus. selection = TextSelection( baseOffset: 0, extentOffset: _value.text.length, ); } else if (!_value.selection.isValid) { // Place cursor at the end if the selection is invalid when we receive focus. selection = TextSelection.collapsed(offset: _value.text.length); } return selection; } void _compositeCallback(Layer layer) { // The callback can be invoked when the layer is detached. // The input connection can be closed by the platform in which case this // widget doesn't rebuild. if (!renderEditable.attached || !_hasInputConnection) { return; } assert(mounted); assert((context as Element).debugIsActive); _updateSizeAndTransform(); } // Must be called after layout. // See https://github.com/flutter/flutter/issues/126312 void _updateSizeAndTransform() { final Size size = renderEditable.size; final Matrix4 transform = renderEditable.getTransformTo(null); _textInputConnection!.setEditableSizeAndTransform(size, transform); } void _schedulePeriodicPostFrameCallbacks([Duration? duration]) { if (!_hasInputConnection) { return; } _updateSelectionRects(); _updateComposingRectIfNeeded(); _updateCaretRectIfNeeded(); SchedulerBinding.instance.addPostFrameCallback( _schedulePeriodicPostFrameCallbacks, debugLabel: 'EditableText.postFrameCallbacks', ); } _ScribbleCacheKey? _scribbleCacheKey; void _updateSelectionRects({bool force = false}) { if (!_stylusHandwritingEnabled || defaultTargetPlatform != TargetPlatform.iOS) { return; } final ScrollDirection scrollDirection = _scrollController.position.userScrollDirection; if (scrollDirection != ScrollDirection.idle) { return; } final InlineSpan inlineSpan = renderEditable.text!; final double? lineHeightScaleFactor = MediaQuery.maybeLineHeightScaleFactorOverrideOf(context); final TextScaler effectiveTextScaler = switch (( widget.textScaler, widget.textScaleFactor, )) { (final TextScaler textScaler, _) => textScaler, (null, final double textScaleFactor) => TextScaler.linear( textScaleFactor, ), (null, null) => MediaQuery.textScalerOf(context), }; final newCacheKey = _ScribbleCacheKey( inlineSpan: inlineSpan, textAlign: widget.textAlign, textDirection: _textDirection, textScaler: effectiveTextScaler, textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), locale: widget.locale, structStyle: widget.strutStyle.merge( StrutStyle(height: lineHeightScaleFactor), ), placeholder: _placeholderLocation, size: renderEditable.size, ); final RenderComparison comparison = force ? RenderComparison.layout : _scribbleCacheKey?.compare(newCacheKey) ?? RenderComparison.layout; if (comparison.index < RenderComparison.layout.index) { return; } _scribbleCacheKey = newCacheKey; final rects = []; var graphemeStart = 0; // Can't use _value.text here: the controller value could change between // frames. final String plainText = inlineSpan.toPlainText( includeSemanticsLabels: false, ); final characterRange = CharacterRange(plainText); while (characterRange.moveNext()) { final int graphemeEnd = graphemeStart + characterRange.current.length; final List boxes = renderEditable.getBoxesForSelection( TextSelection(baseOffset: graphemeStart, extentOffset: graphemeEnd), ); final TextBox? box = boxes.isEmpty ? null : boxes.first; if (box != null) { final Rect paintBounds = renderEditable.paintBounds; // Stop early when characters are already below the bottom edge of the // RenderEditable, regardless of its clipBehavior. if (paintBounds.bottom <= box.top) { break; } // Include any TextBox which intersects with the RenderEditable. if (paintBounds.left <= box.right && box.left <= paintBounds.right && paintBounds.top <= box.bottom) { // At least some part of the letter is visible within the text field. rects.add( SelectionRect( position: graphemeStart, bounds: box.toRect(), direction: box.direction, ), ); } } graphemeStart = graphemeEnd; } _textInputConnection!.setSelectionRects(rects); } // Sends the current composing rect to the embedder's text input plugin. // // In cases where the composing rect hasn't been updated in the embedder due // to the lag of asynchronous messages over the channel, the position of the // current caret rect is used instead. // // See: [_updateCaretRectIfNeeded] void _updateComposingRectIfNeeded() { final TextRange composingRange = _value.composing; assert(mounted); Rect? composingRect = renderEditable.getRectForComposingRange( composingRange, ); // Send the caret location instead if there's no marked text yet. if (composingRect == null) { final int offset = composingRange.isValid ? composingRange.start : 0; composingRect = renderEditable.getLocalRectForCaret( TextPosition(offset: offset), ); } _textInputConnection!.setComposingRect(composingRect); } // Sends the current caret rect to the embedder's text input plugin. // // The position of the caret rect is updated periodically such that if the // user initiates composing input, the current cursor rect can be used for // the first character until the composing rect can be sent. // // On selection changes, the start of the selection is used. This ensures // that regardless of the direction the selection was created, the cursor is // set to the position where next text input occurs. This position is used to // position the IME's candidate selection menu. // // See: [_updateComposingRectIfNeeded] void _updateCaretRectIfNeeded() { final TextSelection? selection = renderEditable.selection; if (selection == null || !selection.isValid) { return; } final currentTextPosition = TextPosition(offset: selection.start); final Rect caretRect = renderEditable.getLocalRectForCaret( currentTextPosition, ); _textInputConnection!.setCaretRect(caretRect); } TextDirection get _textDirection => widget.textDirection ?? Directionality.of(context); /// The renderer for this widget's descendant. /// /// This property is typically used to notify the renderer of input gestures /// when [RenderEditable.ignorePointer] is true. late final RenderEditable renderEditable = _editableKey.currentContext!.findRenderObject()! as RenderEditable; @override TextEditingValue get textEditingValue => _value; double get _devicePixelRatio => MediaQuery.devicePixelRatioOf(context); @override void userUpdateTextEditingValue( TextEditingValue value, SelectionChangedCause? cause, ) { // Compare the current TextEditingValue with the pre-format new // TextEditingValue value, in case the formatter would reject the change. final shouldShowCaret = widget.readOnly ? _value.selection != value.selection : _value != value; if (shouldShowCaret) { scheduleShowCaretOnScreen(withAnimation: true); } // Even if the value doesn't change, it may be necessary to focus and build // the selection overlay. For example, this happens when right clicking an // unfocused field that previously had a selection in the same spot. if (value == textEditingValue) { if (!widget.focusNode.hasFocus) { _flagInternalFocus(); widget.focusNode.requestFocus(); _selectionOverlay ??= _createSelectionOverlay(); } return; } _formatAndSetValue(value, cause, userInteraction: true); } @override void bringIntoView(TextPosition position) { final Rect localRect = renderEditable.getLocalRectForCaret(position); final RevealedOffset targetOffset = _getOffsetToRevealCaret(localRect); _scrollController.jumpTo(targetOffset.offset); renderEditable.showOnScreen(rect: targetOffset.rect); } /// Shows the selection toolbar at the location of the current cursor. /// /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar /// is already shown, or when no text selection currently exists. @override bool showToolbar() { // Web is using native dom elements to enable clipboard functionality of the // context menu: copy, paste, select, cut. It might also provide additional // functionality depending on the browser (such as translate). Due to this, // we should not show a Flutter toolbar for the editable text elements // unless the browser's context menu is explicitly disabled. if (_webContextMenuEnabled) { return false; } if (_selectionOverlay == null) { return false; } if (_selectionOverlay!.toolbarIsVisible) { return false; } _liveTextInputStatus?.update(); clipboardStatus.update(); _selectionOverlay!.showToolbar(); // Listen to parent scroll events when the toolbar is visible so it can be // hidden during a scroll on supported platforms. if (_platformSupportsFadeOnScroll) { _listeningToScrollNotificationObserver = true; _scrollNotificationObserver?.removeListener( _handleContextMenuOnParentScroll, ); _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); _scrollNotificationObserver?.addListener( _handleContextMenuOnParentScroll, ); } return true; } @override void hideToolbar([bool hideHandles = true]) { // Stop listening to parent scroll events when toolbar is hidden. _disposeScrollNotificationObserver(); if (hideHandles) { // Hide the handles and the toolbar. _selectionOverlay?.hide(); } else if (_selectionOverlay?.toolbarIsVisible ?? false) { // Hide only the toolbar but not the handles. _selectionOverlay?.hideToolbar(); } } /// Toggles the visibility of the toolbar. void toggleToolbar([bool hideHandles = true]) { final TextSelectionOverlay selectionOverlay = _selectionOverlay ??= _createSelectionOverlay(); if (selectionOverlay.toolbarIsVisible) { hideToolbar(hideHandles); } else { showToolbar(); } } /// Shows toolbar with spell check suggestions of misspelled words that are /// available for click-and-replace. bool showSpellCheckSuggestionsToolbar() { // Spell check suggestions toolbars are intended to be shown on non-web // platforms. Additionally, the Cupertino style toolbar can't be drawn on // the web with the HTML renderer due to // https://github.com/flutter/flutter/issues/123560. if (!spellCheckEnabled || _webContextMenuEnabled || widget.readOnly || _selectionOverlay == null || !_spellCheckResultsReceived || findSuggestionSpanAtCursorIndex( textEditingValue.selection.extentOffset, ) == null) { // Only attempt to show the spell check suggestions toolbar if there // is a toolbar specified and spell check suggestions available to show. return false; } assert( _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder != null, 'spellCheckSuggestionsToolbarBuilder must be defined in ' 'SpellCheckConfiguration to show a toolbar with spell check ' 'suggestions', ); _selectionOverlay!.showSpellCheckSuggestionsToolbar((BuildContext context) { return _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder!( context, this, ); }); return true; } /// Shows the magnifier at the position given by `positionToShow`, /// if no magnifier exists. /// /// Updates the magnifier to the position given by `positionToShow`, /// if a magnifier exits. /// /// Does nothing if a magnifier couldn't be shown, such as when the selection /// overlay does not currently exist. void showMagnifier(Offset positionToShow) { if (_selectionOverlay == null) { return; } if (_selectionOverlay!.magnifierExists) { _selectionOverlay!.updateMagnifier(positionToShow); } else { _selectionOverlay!.showMagnifier(positionToShow); } } /// Hides the magnifier. void hideMagnifier() { if (_selectionOverlay == null) { return; } _selectionOverlay!.hideMagnifier(); } // Tracks the location a [_ScribblePlaceholder] should be rendered in the // text. // // A value of -1 indicates there should be no placeholder, otherwise the // value should be between 0 and the length of the text, inclusive. int _placeholderLocation = -1; @override void insertTextPlaceholder(Size size) { if (!_stylusHandwritingEnabled) { return; } if (!widget.controller.selection.isValid) { return; } setState(() { _placeholderLocation = _value.text.length - widget.controller.selection.end; }); } @override void removeTextPlaceholder() { if (!_stylusHandwritingEnabled || _placeholderLocation == -1) { return; } setState(() { _placeholderLocation = -1; }); } @override void performSelector(String selectorName) { final Intent? intent = intentForMacOSSelector(selectorName); if (intent != null) { final BuildContext? primaryContext = primaryFocus?.context; if (primaryContext != null) { Actions.invoke(primaryContext, intent); } } } @override String get autofillId => 'EditableText-$hashCode'; int? _viewId; @override TextInputConfiguration get textInputConfiguration { final List? autofillHints = widget.autofillHints?.toList( growable: false, ); final AutofillConfiguration autofillConfiguration = autofillHints != null ? AutofillConfiguration( uniqueIdentifier: autofillId, autofillHints: autofillHints, currentEditingValue: currentTextEditingValue, ) : AutofillConfiguration.disabled; _viewId = View.of(context).viewId; return TextInputConfiguration( enableDeltaModel: true, viewId: _viewId, inputType: widget.keyboardType, readOnly: widget.readOnly, obscureText: widget.obscureText, autocorrect: widget.autocorrect, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, enableSuggestions: widget.enableSuggestions, enableInteractiveSelection: widget._userSelectionEnabled, inputAction: widget.textInputAction ?? (widget.keyboardType == TextInputType.multiline ? TextInputAction.newline : TextInputAction.done), textCapitalization: widget.textCapitalization, keyboardAppearance: widget.keyboardAppearance, autofillConfiguration: autofillConfiguration, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, allowedMimeTypes: widget.contentInsertionConfiguration == null ? const [] : widget.contentInsertionConfiguration!.allowedMimeTypes, hintLocales: widget.hintLocales, ); } @override void autofill(TextEditingValue value) => updateEditingValue(value); // null if no promptRect should be shown. TextRange? _currentPromptRectRange; @override void showAutocorrectionPromptRect(int start, int end) { setState(() { _currentPromptRectRange = TextRange(start: start, end: end); }); } VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) { return widget.selectionEnabled && _hasFocus && (widget.selectionControls is TextSelectionHandleControls ? copyEnabled : copyEnabled && (widget.selectionControls?.canCopy(this) ?? false)) ? () { controls?.handleCopy(this); copySelection(SelectionChangedCause.toolbar); } : null; } VoidCallback? _semanticsOnCut(TextSelectionControls? controls) { return widget.selectionEnabled && _hasFocus && (widget.selectionControls is TextSelectionHandleControls ? cutEnabled : cutEnabled && (widget.selectionControls?.canCut(this) ?? false)) ? () { controls?.handleCut(this); cutSelection(SelectionChangedCause.toolbar); } : null; } VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) { return widget.selectionEnabled && _hasFocus && (widget.selectionControls is TextSelectionHandleControls ? pasteEnabled : pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false)) && (clipboardStatus.value == ClipboardStatus.pasteable) ? () { controls?.handlePaste(this); pasteText(SelectionChangedCause.toolbar); } : null; } // Returns the closest boundary location to `extent` but not including `extent` // itself (unless already at the start/end of the text), in the direction // specified by `forward`. TextPosition _moveBeyondTextBoundary( TextPosition extent, bool forward, TextBoundary textBoundary, ) { assert(extent.offset >= 0); final int newOffset = forward ? textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? _value.text.length // if x is a boundary defined by `textBoundary`, most textBoundaries (except // LineBreaker) guarantees `x == textBoundary.getLeadingTextBoundaryAt(x)`. // Use x - 1 here to make sure we don't get stuck at the fixed point x. : textBoundary.getLeadingTextBoundaryAt(extent.offset - 1) ?? 0; return TextPosition(offset: newOffset); } // Returns the closest boundary location to `extent`, including `extent` // itself, in the direction specified by `forward`. // // This method returns a fixed point of itself: applying `_toTextBoundary` // again on the returned TextPosition gives the same TextPosition. It's used // exclusively for handling line boundaries, since performing "move to line // start" more than once usually doesn't move you to the previous line. TextPosition _moveToTextBoundary( TextPosition extent, bool forward, TextBoundary textBoundary, ) { assert(extent.offset >= 0); final int caretOffset; switch (extent.affinity) { case TextAffinity.upstream: if (extent.offset < 1 && !forward) { assert(extent.offset == 0); return const TextPosition(offset: 0); } // When the text affinity is upstream, the caret is associated with the // grapheme before the code unit at `extent.offset`. // TODO(LongCatIsLooong): don't assume extent.offset is at a grapheme // boundary, and do this instead: // final int graphemeStart = CharacterRange.at(string, extent.offset).stringBeforeLength - 1; caretOffset = math.max(0, extent.offset - 1); case TextAffinity.downstream: caretOffset = extent.offset; } // The line boundary range does not include some control characters // (most notably, Line Feed), in which case there's // `x ∉ getTextBoundaryAt(x)`. In case `caretOffset` points to one such // control character, we define that these control characters themselves are // still part of the previous line, but also exclude them from the // line boundary range since they're non-printing. IOW, no additional // processing needed since the LineBoundary class does exactly that. return forward ? TextPosition( offset: textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? _value.text.length, affinity: TextAffinity.upstream, ) : TextPosition( offset: textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? 0, ); } // --------------------------- Text Editing Actions --------------------------- TextBoundary _characterBoundary() => widget.obscureText ? _CodePointBoundary(_value.text) : CharacterBoundary(_value.text); TextBoundary _nextWordBoundary() => widget.obscureText ? _documentBoundary() : renderEditable.wordBoundaries.moveByWordBoundary; TextBoundary _linebreak() => widget.obscureText ? _documentBoundary() : LineBoundary(renderEditable); TextBoundary _paragraphBoundary() => ParagraphBoundary(_value.text); TextBoundary _documentBoundary() => DocumentBoundary(_value.text); Action _makeOverridable(Action defaultAction) { return Action.overridable( context: context, defaultAction: defaultAction, ); } /// Transpose the characters immediately before and after the current /// collapsed selection. /// /// When the cursor is at the end of the text, transposes the last two /// characters, if they exist. /// /// When the cursor is at the start of the text, does nothing. void _transposeCharacters(TransposeCharactersIntent intent) { if (_value.text.characters.length <= 1 || !_value.selection.isCollapsed || _value.selection.baseOffset == 0) { return; } final String text = _value.text; final TextSelection selection = _value.selection; final atEnd = selection.baseOffset == text.length; final transposing = CharacterRange.at(text, selection.baseOffset); if (atEnd) { transposing.moveBack(2); } else { transposing ..moveBack() ..expandNext(); } assert(transposing.currentCharacters.length == 2); userUpdateTextEditingValue( TextEditingValue( text: transposing.stringBefore + transposing.currentCharacters.last + transposing.currentCharacters.first + transposing.stringAfter, selection: TextSelection.collapsed( offset: transposing.stringBeforeLength + transposing.current.length, ), ), SelectionChangedCause.keyboard, ); } late final Action _transposeCharactersAction = CallbackAction(onInvoke: _transposeCharacters); void _replaceText(ReplaceTextIntent intent) { final TextEditingValue oldValue = _value; // bggRGjQaUbCoE _replaceText widget.controller.syncRichText( intent.replacementText.isEmpty ? TextEditingDeltaDeletion( oldText: oldValue.text, deletedRange: intent.replacementRange, selection: TextSelection.collapsed( offset: intent.replacementRange.start, ), composing: TextRange.empty, ) : TextEditingDeltaReplacement( oldText: oldValue.text, replacementText: intent.replacementText, replacedRange: intent.replacementRange, selection: TextSelection.collapsed( offset: intent.replacementRange.start, ), composing: TextRange.empty, ), ); final newValue = oldValue.copyWith( text: widget.controller.plainText, selection: widget.controller.newSelection, composing: TextRange.empty, ); userUpdateTextEditingValue(newValue, intent.cause); // If there's no change in text and selection (e.g. when selecting and // pasting identical text), the widget won't be rebuilt on value update. // Handle this by calling _didChangeTextEditingValue() so caret and scroll // updates can happen. if (newValue == oldValue) { _didChangeTextEditingValue(); } } late final Action _replaceTextAction = CallbackAction( onInvoke: _replaceText, ); // Scrolls either to the beginning or end of the document depending on the // intent's `forward` parameter. void _scrollToDocumentBoundary(ScrollToDocumentBoundaryIntent intent) { if (intent.forward) { bringIntoView(TextPosition(offset: _value.text.length)); } else { bringIntoView(const TextPosition(offset: 0)); } } /// Handles [ScrollIntent] by scrolling the [Scrollable] inside of /// [EditableText]. void _scroll(ScrollIntent intent) { if (intent.type != ScrollIncrementType.page) { return; } final ScrollPosition position = _scrollController.position; if (widget.maxLines == 1) { _scrollController.jumpTo(position.maxScrollExtent); return; } // If the field isn't scrollable, do nothing. For example, when the lines of // text is less than maxLines, the field has nothing to scroll. if (position.maxScrollExtent == 0.0 && position.minScrollExtent == 0.0) { return; } final state = _scrollableKey.currentState as ScrollableState?; final double increment = ScrollAction.getDirectionalIncrement( state!, intent, ); final double destination = clampDouble( position.pixels + increment, position.minScrollExtent, position.maxScrollExtent, ); if (destination == position.pixels) { return; } _scrollController.jumpTo(destination); } /// Extend the selection down by page if the `forward` parameter is true, or /// up by page otherwise. void _extendSelectionByPage(ExtendSelectionByPageIntent intent) { if (widget.maxLines == 1) { return; } final TextSelection nextSelection; final Rect extentRect = renderEditable.getLocalRectForCaret( _value.selection.extent, ); final state = _scrollableKey.currentState as ScrollableState?; final double increment = ScrollAction.getDirectionalIncrement( state!, ScrollIntent( direction: intent.forward ? AxisDirection.down : AxisDirection.up, type: ScrollIncrementType.page, ), ); final ScrollPosition position = _scrollController.position; if (intent.forward) { if (_value.selection.extentOffset >= _value.text.length) { return; } final nextExtentOffset = Offset( extentRect.left, extentRect.top + increment, ); final double height = position.maxScrollExtent + renderEditable.size.height; final TextPosition nextExtent = nextExtentOffset.dy + position.pixels >= height ? TextPosition(offset: _value.text.length) : renderEditable.getPositionForPoint( renderEditable.localToGlobal(nextExtentOffset), ); nextSelection = _value.selection.copyWith( extentOffset: nextExtent.offset, ); } else { if (_value.selection.extentOffset <= 0) { return; } final nextExtentOffset = Offset( extentRect.left, extentRect.top + increment, ); final TextPosition nextExtent = nextExtentOffset.dy + position.pixels <= 0 ? const TextPosition(offset: 0) : renderEditable.getPositionForPoint( renderEditable.localToGlobal(nextExtentOffset), ); nextSelection = _value.selection.copyWith( extentOffset: nextExtent.offset, ); } bringIntoView(nextSelection.extent); userUpdateTextEditingValue( _value.copyWith(selection: nextSelection), SelectionChangedCause.keyboard, ); } void _updateSelection(UpdateSelectionIntent intent) { assert( intent.newSelection.start <= intent.currentTextEditingValue.text.length, 'invalid selection: ${intent.newSelection}: it must not exceed the current text length ${intent.currentTextEditingValue.text.length}', ); assert( intent.newSelection.end <= intent.currentTextEditingValue.text.length, 'invalid selection: ${intent.newSelection}: it must not exceed the current text length ${intent.currentTextEditingValue.text.length}', ); bringIntoView(intent.newSelection.extent); // bggRGjQaUbCoE keyboard TextSelection newSelection = intent.newSelection; if (newSelection.isCollapsed) { newSelection = widget.controller.keyboardOffset(newSelection); } else { newSelection = widget.controller.keyboardOffsets(newSelection); } userUpdateTextEditingValue( intent.currentTextEditingValue.copyWith(selection: newSelection), intent.cause, ); } late final Action _updateSelectionAction = CallbackAction(onInvoke: _updateSelection); late final _UpdateTextSelectionVerticallyAction< DirectionalCaretMovementIntent > _verticalSelectionUpdateAction = _UpdateTextSelectionVerticallyAction( this, ); Object? _hideToolbarIfVisible(DismissIntent intent) { if (_selectionOverlay?.toolbarIsVisible ?? false) { hideToolbar(false); return null; } return Actions.invoke(context, intent); } void _onTapOutside(BuildContext context, PointerDownEvent event) { _hadFocusOnTapDown = true; if (widget.onTapOutside != null) { widget.onTapOutside!(event); } else { _defaultOnTapOutside(context, event); } } void _onTapUpOutside(BuildContext context, PointerUpEvent event) { if (!_hadFocusOnTapDown) { return; } // Reset to false so that subsequent events doesn't trigger the callback based on old information. _hadFocusOnTapDown = false; if (widget.onTapUpOutside != null) { widget.onTapUpOutside!(event); } else { _defaultOnTapUpOutside(context, event); } } /// The default behavior used if [EditableText.onTapOutside] is null. /// /// The `event` argument is the [PointerDownEvent] that caused the notification. void _defaultOnTapOutside(BuildContext context, PointerDownEvent event) { Actions.invoke( context, EditableTextTapOutsideIntent( focusNode: widget.focusNode, pointerDownEvent: event, ), ); } /// The default behavior used if [EditableText.onTapUpOutside] is null. /// /// The `event` argument is the [PointerUpEvent] that caused the notification. void _defaultOnTapUpOutside(BuildContext context, PointerUpEvent event) { Actions.invoke( context, EditableTextTapUpOutsideIntent( focusNode: widget.focusNode, pointerUpEvent: event, ), ); } late final Map> _actions = >{ DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), ReplaceTextIntent: _replaceTextAction, UpdateSelectionIntent: _updateSelectionAction, DirectionalFocusIntent: DirectionalFocusAction.forTextField(), DismissIntent: CallbackAction( onInvoke: _hideToolbarIfVisible, ), // Delete DeleteCharacterIntent: _makeOverridable( _DeleteTextAction( this, _characterBoundary, _moveBeyondTextBoundary, ), ), DeleteToNextWordBoundaryIntent: _makeOverridable( _DeleteTextAction( this, _nextWordBoundary, _moveBeyondTextBoundary, ), ), DeleteToLineBreakIntent: _makeOverridable( _DeleteTextAction( this, _linebreak, _moveToTextBoundary, ), ), // Extend/Move Selection ExtendSelectionByCharacterIntent: _makeOverridable( _UpdateTextSelectionAction( this, _characterBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: false, ), ), ExtendSelectionByPageIntent: _makeOverridable( CallbackAction( onInvoke: _extendSelectionByPage, ), ), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( _UpdateTextSelectionAction( this, _nextWordBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true, ), ), ExtendSelectionToNextParagraphBoundaryIntent: _makeOverridable( _UpdateTextSelectionAction( this, _paragraphBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true, ), ), ExtendSelectionToLineBreakIntent: _makeOverridable( _UpdateTextSelectionAction( this, _linebreak, _moveToTextBoundary, ignoreNonCollapsedSelection: true, ), ), ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable( _verticalSelectionUpdateAction, ), ExtendSelectionVerticallyToAdjacentPageIntent: _makeOverridable( _verticalSelectionUpdateAction, ), ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent: _makeOverridable( _UpdateTextSelectionAction< ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent >( this, _paragraphBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true, ), ), ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( _UpdateTextSelectionAction( this, _documentBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true, ), ), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( _UpdateTextSelectionAction< ExtendSelectionToNextWordBoundaryOrCaretLocationIntent >( this, _nextWordBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true, ), ), ScrollToDocumentBoundaryIntent: _makeOverridable( _WebComposingDisablingCallbackAction( this, onInvoke: _scrollToDocumentBoundary, ), ), ScrollIntent: CallbackAction(onInvoke: _scroll), // Expand Selection ExpandSelectionToLineBreakIntent: _makeOverridable( _UpdateTextSelectionAction( this, _linebreak, _moveToTextBoundary, ignoreNonCollapsedSelection: true, isExpand: true, ), ), ExpandSelectionToDocumentBoundaryIntent: _makeOverridable( _UpdateTextSelectionAction( this, _documentBoundary, _moveToTextBoundary, ignoreNonCollapsedSelection: true, isExpand: true, extentAtIndex: true, ), ), // Copy Paste SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), PasteTextIntent: _makeOverridable(_PasteSelectionAction(this)), TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction), EditableTextTapOutsideIntent: _makeOverridable( _EditableTextTapOutsideAction(), ), EditableTextTapUpOutsideIntent: _makeOverridable( _EditableTextTapUpOutsideAction(), ), }; @protected @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); super.build(context); // See AutomaticKeepAliveClientMixin. final TextSelectionControls? controls = widget.selectionControls; final TextScaler effectiveTextScaler = switch (( widget.textScaler, widget.textScaleFactor, )) { (final TextScaler textScaler, _) => textScaler, (null, final double textScaleFactor) => TextScaler.linear( textScaleFactor, ), (null, null) => MediaQuery.textScalerOf(context), }; final double? lineHeightScaleFactor = MediaQuery.maybeLineHeightScaleFactorOverrideOf(context); final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf( context, ); final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context); final ui.SemanticsInputType inputType; switch (widget.keyboardType) { case TextInputType.phone: inputType = ui.SemanticsInputType.phone; case TextInputType.url: inputType = ui.SemanticsInputType.url; case TextInputType.emailAddress: inputType = ui.SemanticsInputType.email; default: inputType = ui.SemanticsInputType.text; } return _CompositionCallback( compositeCallback: _compositeCallback, enabled: _hasInputConnection, child: Actions( actions: _actions, child: Builder( builder: (BuildContext context) { return TextFieldTapRegion( groupId: widget.groupId, onTapOutside: _hasFocus ? (PointerDownEvent event) => _onTapOutside(context, event) : null, onTapUpOutside: (PointerUpEvent event) => _onTapUpOutside(context, event), debugLabel: kReleaseMode ? null : 'EditableText', child: MouseRegion( cursor: widget.mouseCursor ?? SystemMouseCursors.text, child: Focus( focusNode: widget.focusNode, includeSemantics: false, debugLabel: kReleaseMode ? null : 'EditableText', child: NotificationListener( onNotification: (ScrollNotification notification) { _handleContextMenuOnScroll(notification); _scribbleCacheKey = null; return false; }, child: Scrollable( key: _scrollableKey, excludeFromSemantics: true, axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, controller: _scrollController, // On iOS a single-line TextField should not scroll. physics: widget.scrollPhysics ?? (!_isMultiline && defaultTargetPlatform == TargetPlatform.iOS ? const _NeverUserScrollableScrollPhysics() : null), dragStartBehavior: widget.dragStartBehavior, restorationId: widget.restorationId, // If a ScrollBehavior is not provided, only apply scrollbars when // multiline. The overscroll indicator should not be applied in // either case, glowing or stretching. scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of( context, ).copyWith( scrollbars: _isMultiline, overscroll: false, ), viewportBuilder: (BuildContext context, ViewportOffset offset) { return CompositedTransformTarget( link: _toolbarLayerLink, child: Semantics( inputType: inputType, onCopy: _semanticsOnCopy(controls), onCut: _semanticsOnCut(controls), onPaste: _semanticsOnPaste(controls), child: _ScribbleFocusable( editableKey: _editableKey, enabled: _stylusHandwritingEnabled, focusNode: widget.focusNode, updateSelectionRects: () { _openInputConnection(); _updateSelectionRects(force: true); }, child: SizeChangedLayoutNotifier( child: _Editable( key: _editableKey, controller: widget.controller, startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, inlineSpan: _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( lineHeightScaleFactor: lineHeightScaleFactor, letterSpacing: letterSpacing, wordSpacing: wordSpacing, textSpan: buildTextSpan(), ), value: _value, cursorColor: _cursorColor, backgroundCursorColor: widget.backgroundCursorColor, showCursor: _cursorVisibilityNotifier, forceLine: widget.forceLine, readOnly: widget.readOnly, hasFocus: _hasFocus, maxLines: widget.maxLines, minLines: widget.minLines, expands: widget.expands, strutStyle: widget.strutStyle.merge( StrutStyle( height: lineHeightScaleFactor, ), ), selectionColor: _selectionOverlay ?.spellCheckToolbarIsVisible ?? false ? _spellCheckConfiguration .misspelledSelectionColor ?? widget.selectionColor : widget.selectionColor, textScaler: effectiveTextScaler, textAlign: widget.textAlign, textDirection: _textDirection, locale: widget.locale, textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf( context, ), textWidthBasis: widget.textWidthBasis, obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, offset: offset, rendererIgnoresPointer: widget.rendererIgnoresPointer, cursorWidth: widget.cursorWidth, cursorHeight: widget.cursorHeight, cursorRadius: widget.cursorRadius, cursorOffset: widget.cursorOffset ?? Offset.zero, selectionHeightStyle: widget.selectionHeightStyle, selectionWidthStyle: widget.selectionWidthStyle, paintCursorAboveText: widget.paintCursorAboveText, enableInteractiveSelection: widget._userSelectionEnabled, textSelectionDelegate: this, devicePixelRatio: _devicePixelRatio, promptRectRange: _currentPromptRectRange, promptRectColor: widget.autocorrectionTextRectColor, clipBehavior: widget.clipBehavior, ), ), ), ), ); }, ), ), ), ), ); }, ), ), ); } /// Builds [TextSpan] from current editing value. /// /// By default makes text in composing range appear as underlined. /// Descendants can override this method to customize appearance of text. TextSpan buildTextSpan() { if (widget.obscureText) { String text = _value.text; text = widget.obscuringCharacter * text.length; // Reveal the latest character in an obscured field only on mobile. const mobilePlatforms = { TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.iOS, }; final bool brieflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword && mobilePlatforms.contains(defaultTargetPlatform); if (brieflyShowPassword) { final int? o = _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null; if (o != null && o >= 0 && o < text.length) { text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1)); } } return TextSpan(style: _style, text: text); } if (_placeholderLocation >= 0 && _placeholderLocation <= _value.text.length) { final placeholders = <_ScribblePlaceholder>[]; final int placeholderLocation = _value.text.length - _placeholderLocation; if (_isMultiline) { // The zero size placeholder here allows the line to break and keep the caret on the first line. placeholders ..add( const _ScribblePlaceholder( child: SizedBox.shrink(), size: Size.zero, ), ) ..add( _ScribblePlaceholder( child: const SizedBox.shrink(), size: Size(renderEditable.size.width, 0.0), ), ); } else { placeholders.add( const _ScribblePlaceholder( child: SizedBox.shrink(), size: Size(100.0, 0.0), ), ); } return TextSpan( style: _style, children: [ TextSpan(text: _value.text.substring(0, placeholderLocation)), ...placeholders, TextSpan(text: _value.text.substring(placeholderLocation)), ], ); } final bool withComposing = !widget.readOnly && _hasFocus; if (_spellCheckResultsReceived) { // If the composing range is out of range for the current text, ignore it to // preserve the tree integrity, otherwise in release mode a RangeError will // be thrown and this EditableText will be built with a broken subtree. assert( !_value.composing.isValid || !withComposing || _value.isComposingRangeValid, ); final bool composingRegionOutOfRange = !_value.isComposingRangeValid || !withComposing; return buildTextSpanWithSpellCheckSuggestions( _value, composingRegionOutOfRange, _style, _spellCheckConfiguration.misspelledTextStyle!, spellCheckResults!, ); } // Read only mode should not paint text composing. return widget.controller.buildTextSpan( context: context, style: _style, withComposing: withComposing, ); } } class _Editable extends MultiChildRenderObjectWidget { _Editable({ super.key, required this.inlineSpan, required this.value, required this.startHandleLayerLink, required this.endHandleLayerLink, this.cursorColor, this.backgroundCursorColor, required this.showCursor, required this.forceLine, required this.readOnly, this.textHeightBehavior, required this.textWidthBasis, required this.hasFocus, required this.maxLines, this.minLines, required this.expands, this.strutStyle, this.selectionColor, required this.textScaler, required this.textAlign, required this.textDirection, this.locale, required this.obscuringCharacter, required this.obscureText, required this.offset, this.rendererIgnoresPointer = false, required this.cursorWidth, this.cursorHeight, this.cursorRadius, required this.cursorOffset, required this.paintCursorAboveText, ui.BoxHeightStyle? selectionHeightStyle, ui.BoxWidthStyle? selectionWidthStyle, this.enableInteractiveSelection = true, required this.textSelectionDelegate, required this.devicePixelRatio, this.promptRectRange, this.promptRectColor, required this.clipBehavior, required this.controller, }) : selectionHeightStyle = selectionHeightStyle ?? EditableText.defaultSelectionHeightStyle, selectionWidthStyle = selectionWidthStyle ?? EditableText.defaultSelectionWidthStyle, super( children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler), ); final InlineSpan inlineSpan; final TextEditingValue value; final Color? cursorColor; final LayerLink startHandleLayerLink; final LayerLink endHandleLayerLink; final Color? backgroundCursorColor; final ValueNotifier showCursor; final bool forceLine; final bool readOnly; final bool hasFocus; final int? maxLines; final int? minLines; final bool expands; final StrutStyle? strutStyle; final Color? selectionColor; final TextScaler textScaler; final TextAlign textAlign; final TextDirection textDirection; final Locale? locale; final String obscuringCharacter; final bool obscureText; final TextHeightBehavior? textHeightBehavior; final TextWidthBasis textWidthBasis; final ViewportOffset offset; final bool rendererIgnoresPointer; final double cursorWidth; final double? cursorHeight; final Radius? cursorRadius; final Offset cursorOffset; final bool paintCursorAboveText; final ui.BoxHeightStyle selectionHeightStyle; final ui.BoxWidthStyle selectionWidthStyle; final bool enableInteractiveSelection; final TextSelectionDelegate textSelectionDelegate; final double devicePixelRatio; final TextRange? promptRectRange; final Color? promptRectColor; final Clip clipBehavior; final RichTextEditingController controller; @override RenderEditable createRenderObject(BuildContext context) { return RenderEditable( controller: controller, text: inlineSpan, cursorColor: cursorColor, startHandleLayerLink: startHandleLayerLink, endHandleLayerLink: endHandleLayerLink, backgroundCursorColor: backgroundCursorColor, showCursor: showCursor, forceLine: forceLine, readOnly: readOnly, hasFocus: hasFocus, maxLines: maxLines, minLines: minLines, expands: expands, strutStyle: strutStyle, selectionColor: selectionColor, textScaler: textScaler, textAlign: textAlign, textDirection: textDirection, locale: locale ?? Localizations.maybeLocaleOf(context), selection: value.selection, offset: offset, ignorePointer: rendererIgnoresPointer, obscuringCharacter: obscuringCharacter, obscureText: obscureText, textHeightBehavior: textHeightBehavior, textWidthBasis: textWidthBasis, cursorWidth: cursorWidth, cursorHeight: cursorHeight, cursorRadius: cursorRadius, cursorOffset: cursorOffset, paintCursorAboveText: paintCursorAboveText, selectionHeightStyle: selectionHeightStyle, selectionWidthStyle: selectionWidthStyle, enableInteractiveSelection: enableInteractiveSelection, textSelectionDelegate: textSelectionDelegate, devicePixelRatio: devicePixelRatio, promptRectRange: promptRectRange, promptRectColor: promptRectColor, clipBehavior: clipBehavior, ); } @override void updateRenderObject(BuildContext context, RenderEditable renderObject) { renderObject ..text = inlineSpan ..cursorColor = cursorColor ..startHandleLayerLink = startHandleLayerLink ..endHandleLayerLink = endHandleLayerLink ..backgroundCursorColor = backgroundCursorColor ..showCursor = showCursor ..forceLine = forceLine ..readOnly = readOnly ..hasFocus = hasFocus ..maxLines = maxLines ..minLines = minLines ..expands = expands ..strutStyle = strutStyle ..selectionColor = selectionColor ..textScaler = textScaler ..textAlign = textAlign ..textDirection = textDirection ..locale = locale ?? Localizations.maybeLocaleOf(context) ..selection = value.selection ..offset = offset ..ignorePointer = rendererIgnoresPointer ..textHeightBehavior = textHeightBehavior ..textWidthBasis = textWidthBasis ..obscuringCharacter = obscuringCharacter ..obscureText = obscureText ..cursorWidth = cursorWidth ..cursorHeight = cursorHeight ..cursorRadius = cursorRadius ..cursorOffset = cursorOffset ..selectionHeightStyle = selectionHeightStyle ..selectionWidthStyle = selectionWidthStyle ..enableInteractiveSelection = enableInteractiveSelection ..textSelectionDelegate = textSelectionDelegate ..devicePixelRatio = devicePixelRatio ..paintCursorAboveText = paintCursorAboveText ..promptRectColor = promptRectColor ..clipBehavior = clipBehavior ..setPromptRectRange(promptRectRange); } } class _NeverUserScrollableScrollPhysics extends ScrollPhysics { /// Creates a scroll physics that prevents scrolling with user input, for example /// by dragging, but still allows for programmatic scrolling. const _NeverUserScrollableScrollPhysics({super.parent}); @override _NeverUserScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { return _NeverUserScrollableScrollPhysics(parent: buildParent(ancestor)); } @override bool get allowUserScrolling => false; } @immutable class _ScribbleCacheKey { const _ScribbleCacheKey({ required this.inlineSpan, required this.textAlign, required this.textDirection, required this.textScaler, required this.textHeightBehavior, required this.locale, required this.structStyle, required this.placeholder, required this.size, }); final TextAlign textAlign; final TextDirection textDirection; final TextScaler textScaler; final TextHeightBehavior? textHeightBehavior; final Locale? locale; final StrutStyle structStyle; final int placeholder; final Size size; final InlineSpan inlineSpan; RenderComparison compare(_ScribbleCacheKey other) { if (identical(other, this)) { return RenderComparison.identical; } final bool needsLayout = textAlign != other.textAlign || textDirection != other.textDirection || textScaler != other.textScaler || (textHeightBehavior ?? const TextHeightBehavior()) != (other.textHeightBehavior ?? const TextHeightBehavior()) || locale != other.locale || structStyle != other.structStyle || placeholder != other.placeholder || size != other.size; return needsLayout ? RenderComparison.layout : inlineSpan.compareTo(other.inlineSpan); } } class _ScribbleFocusable extends StatefulWidget { const _ScribbleFocusable({ required this.child, required this.focusNode, required this.editableKey, required this.updateSelectionRects, required this.enabled, }); final Widget child; final FocusNode focusNode; final GlobalKey editableKey; final VoidCallback updateSelectionRects; final bool enabled; @override _ScribbleFocusableState createState() => _ScribbleFocusableState(); } class _ScribbleFocusableState extends State<_ScribbleFocusable> implements ScribbleClient { _ScribbleFocusableState() : _elementIdentifier = (_nextElementIdentifier++).toString(); @override void initState() { super.initState(); if (widget.enabled) { TextInput.registerScribbleElement(elementIdentifier, this); } } @override void didUpdateWidget(_ScribbleFocusable oldWidget) { super.didUpdateWidget(oldWidget); if (!oldWidget.enabled && widget.enabled) { TextInput.registerScribbleElement(elementIdentifier, this); } if (oldWidget.enabled && !widget.enabled) { TextInput.unregisterScribbleElement(elementIdentifier); } } @override void dispose() { TextInput.unregisterScribbleElement(elementIdentifier); super.dispose(); } RenderEditable? get renderEditable => widget.editableKey.currentContext?.findRenderObject() as RenderEditable?; static int _nextElementIdentifier = 1; final String _elementIdentifier; @override String get elementIdentifier => _elementIdentifier; @override void onScribbleFocus(Offset offset) { widget.focusNode.requestFocus(); renderEditable?.selectPositionAt( from: offset, cause: SelectionChangedCause.stylusHandwriting, ); widget.updateSelectionRects(); } @override bool isInScribbleRect(Rect rect) { final Rect calculatedBounds = bounds; if (renderEditable?.readOnly ?? false) { return false; } if (calculatedBounds == Rect.zero) { return false; } if (!calculatedBounds.overlaps(rect)) { return false; } final Rect intersection = calculatedBounds.intersect(rect); final result = HitTestResult(); WidgetsBinding.instance.hitTestInView( result, intersection.center, View.of(context).viewId, ); return result.path.any( (HitTestEntry entry) => entry.target == renderEditable, ); } @override Rect get bounds { final box = context.findRenderObject() as RenderBox?; if (box == null || !mounted || !box.attached) { return Rect.zero; } final Matrix4 transform = box.getTransformTo(null); return MatrixUtils.transformRect( transform, Rect.fromLTRB(0, 0, box.size.width, box.size.height), ); } @override Widget build(BuildContext context) { return widget.child; } } class _ScribblePlaceholder extends WidgetSpan { const _ScribblePlaceholder({required super.child, required this.size}); /// The size of the span, used in place of adding a placeholder size to the [TextPainter]. final Size size; @override void build( ui.ParagraphBuilder builder, { TextScaler textScaler = TextScaler.noScaling, List? dimensions, }) { assert(debugAssertIsValid()); final hasStyle = style != null; if (hasStyle) { builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); } builder.addPlaceholder(size.width, size.height, alignment); if (hasStyle) { builder.pop(); } } } /// A text boundary that uses code points as logical boundaries. /// /// A code point represents a single character. This may be smaller than what is /// represented by a user-perceived character, or grapheme. For example, a /// single grapheme (in this case a Unicode extended grapheme cluster) like /// "👨‍👩‍👦" consists of five code points: the man emoji, a zero /// width joiner, the woman emoji, another zero width joiner, and the boy emoji. /// The [String] has a length of eight because each emoji consists of two code /// units. /// /// Code units are the units by which Dart's String class is measured, which is /// encoded in UTF-16. /// /// See also: /// /// * [String.runes], which deals with code points like this class. /// * [Characters], which deals with graphemes. /// * [CharacterBoundary], which is a [TextBoundary] like this class, but whose /// boundaries are graphemes instead of code points. class _CodePointBoundary extends TextBoundary { const _CodePointBoundary(this._text); final String _text; // Returns true if the given position falls in the center of a surrogate pair. bool _breaksSurrogatePair(int position) { assert(position > 0 && position < _text.length && _text.length > 1); return TextPainter.isHighSurrogate(_text.codeUnitAt(position - 1)) && TextPainter.isLowSurrogate(_text.codeUnitAt(position)); } @override int? getLeadingTextBoundaryAt(int position) { if (_text.isEmpty || position < 0) { return null; } if (position == 0) { return 0; } if (position >= _text.length) { return _text.length; } if (_text.length <= 1) { return position; } return _breaksSurrogatePair(position) ? position - 1 : position; } @override int? getTrailingTextBoundaryAt(int position) { if (_text.isEmpty || position >= _text.length) { return null; } if (position < 0) { return 0; } if (position == _text.length - 1) { return _text.length; } if (_text.length <= 1) { return position; } return _breaksSurrogatePair(position + 1) ? position + 2 : position + 1; } } // ------------------------------- Text Actions ------------------------------- class _DeleteTextAction extends ContextAction { _DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary); final EditableTextState state; final TextBoundary Function() getTextBoundary; final _ApplyTextBoundary _applyTextBoundary; void _hideToolbarIfTextChanged(ReplaceTextIntent intent) { if (state._selectionOverlay == null || !state.selectionOverlay!.toolbarIsVisible) { return; } final TextEditingValue oldValue = intent.currentTextEditingValue; final TextEditingValue newValue = intent.currentTextEditingValue.replaced( intent.replacementRange, intent.replacementText, ); if (oldValue.text != newValue.text) { // Hide the toolbar if the text was changed, but only hide the toolbar // overlay; the selection handle's visibility will be handled // by `_handleSelectionChanged`. state.hideToolbar(false); } } @override Object? invoke(T intent, [BuildContext? context]) { final TextSelection selection = state._value.selection; if (!selection.isValid) { return null; } assert(selection.isValid); // Expands the selection to ensure the range covers full graphemes. final TextBoundary atomicBoundary = state._characterBoundary(); if (!selection.isCollapsed) { // Expands the selection to ensure the range covers full graphemes. final range = TextRange( start: atomicBoundary.getLeadingTextBoundaryAt(selection.start) ?? state._value.text.length, end: atomicBoundary.getTrailingTextBoundaryAt(selection.end - 1) ?? 0, ); final replaceTextIntent = ReplaceTextIntent( state._value, '', range, SelectionChangedCause.keyboard, ); _hideToolbarIfTextChanged(replaceTextIntent); return Actions.invoke(context!, replaceTextIntent); } final int target = _applyTextBoundary( selection.base, intent.forward, getTextBoundary(), ).offset; final TextRange rangeToDelete = TextSelection( baseOffset: intent.forward ? atomicBoundary.getLeadingTextBoundaryAt(selection.baseOffset) ?? state._value.text.length : atomicBoundary.getTrailingTextBoundaryAt( selection.baseOffset - 1, ) ?? 0, extentOffset: target, ); final replaceTextIntent = ReplaceTextIntent( state._value, '', rangeToDelete, SelectionChangedCause.keyboard, ); _hideToolbarIfTextChanged(replaceTextIntent); return Actions.invoke(context!, replaceTextIntent); } @override bool get isActionEnabled => !state.widget.readOnly && state._value.selection.isValid; } class _UpdateTextSelectionAction extends ContextAction { _UpdateTextSelectionAction( this.state, this.getTextBoundary, this.applyTextBoundary, { required this.ignoreNonCollapsedSelection, this.isExpand = false, this.extentAtIndex = false, }); final EditableTextState state; final bool ignoreNonCollapsedSelection; final bool isExpand; final bool extentAtIndex; final TextBoundary Function() getTextBoundary; final _ApplyTextBoundary applyTextBoundary; static const int NEWLINE_CODE_UNIT = 10; // Returns true iff the given position is at a wordwrap boundary in the // upstream position. bool _isAtWordwrapUpstream(TextPosition position) { final end = TextPosition( offset: state.renderEditable.getLineAtOffset(position).end, affinity: TextAffinity.upstream, ); return end == position && end.offset != state.textEditingValue.text.length && state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT; } // Returns true if the given position at a wordwrap boundary in the // downstream position. bool _isAtWordwrapDownstream(TextPosition position) { final start = TextPosition( offset: state.renderEditable.getLineAtOffset(position).start, ); return start == position && start.offset != 0 && state.textEditingValue.text.codeUnitAt(position.offset - 1) != NEWLINE_CODE_UNIT; } @override Object? invoke(T intent, [BuildContext? context]) { final TextSelection selection = state._value.selection; assert(selection.isValid); final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled; if (!selection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) { return Actions.invoke( context!, UpdateSelectionIntent( state._value, TextSelection.collapsed( offset: intent.forward ? selection.end : selection.start, ), SelectionChangedCause.keyboard, ), ); } TextPosition extent = selection.extent; // If continuesAtWrap is true extent and is at the relevant wordwrap, then // move it just to the other side of the wordwrap. if (intent.continuesAtWrap) { if (intent.forward && _isAtWordwrapUpstream(extent)) { extent = TextPosition(offset: extent.offset); } else if (!intent.forward && _isAtWordwrapDownstream(extent)) { extent = TextPosition( offset: extent.offset, affinity: TextAffinity.upstream, ); } } final bool shouldTargetBase = isExpand && (intent.forward ? selection.baseOffset > selection.extentOffset : selection.baseOffset < selection.extentOffset); final TextPosition newExtent = applyTextBoundary( shouldTargetBase ? selection.base : extent, intent.forward, getTextBoundary(), ); final TextSelection newSelection = collapseSelection || (!isExpand && newExtent.offset == selection.baseOffset) ? TextSelection.fromPosition(newExtent) : isExpand ? selection.expandTo(newExtent, extentAtIndex || selection.isCollapsed) : selection.extendTo(newExtent); final bool shouldCollapseToBase = intent.collapseAtReversal && (selection.baseOffset - selection.extentOffset) * (selection.baseOffset - newSelection.extentOffset) < 0; final newRange = shouldCollapseToBase ? TextSelection.fromPosition(selection.base) : newSelection; return Actions.invoke( context!, UpdateSelectionIntent( state._value, newRange, SelectionChangedCause.keyboard, ), ); } @override bool get isActionEnabled { if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) { return false; } return state._value.selection.isValid; } } class _UpdateTextSelectionVerticallyAction< T extends DirectionalCaretMovementIntent > extends ContextAction { _UpdateTextSelectionVerticallyAction(this.state); final EditableTextState state; VerticalCaretMovementRun? _verticalMovementRun; TextSelection? _runSelection; void stopCurrentVerticalRunIfSelectionChanges() { final TextSelection? runSelection = _runSelection; if (runSelection == null) { assert(_verticalMovementRun == null); return; } _runSelection = state._value.selection; final TextSelection currentSelection = state.widget.controller.selection; final bool continueCurrentRun = currentSelection.isValid && currentSelection.isCollapsed && currentSelection.baseOffset == runSelection.baseOffset && currentSelection.extentOffset == runSelection.extentOffset; if (!continueCurrentRun) { _verticalMovementRun = null; _runSelection = null; } } @override void invoke(T intent, [BuildContext? context]) { assert(state._value.selection.isValid); final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled; final TextEditingValue value = state._textEditingValueforTextLayoutMetrics; if (!value.selection.isValid) { return; } if (_verticalMovementRun?.isValid == false) { _verticalMovementRun = null; _runSelection = null; } final VerticalCaretMovementRun currentRun = _verticalMovementRun ?? state.renderEditable.startVerticalCaretMovement( state.renderEditable.selection!.extent, ); final bool shouldMove = intent is ExtendSelectionVerticallyToAdjacentPageIntent ? currentRun.moveByOffset( (intent.forward ? 1.0 : -1.0) * state.renderEditable.size.height, ) : intent.forward ? currentRun.moveNext() : currentRun.movePrevious(); final TextPosition newExtent = shouldMove ? currentRun.current : intent.forward ? TextPosition(offset: value.text.length) : const TextPosition(offset: 0); final TextSelection newSelection = collapseSelection ? TextSelection.fromPosition(newExtent) : value.selection.extendTo(newExtent); Actions.invoke( context!, UpdateSelectionIntent( value, newSelection, SelectionChangedCause.keyboard, ), ); if (state._value.selection == newSelection) { _verticalMovementRun = currentRun; _runSelection = newSelection; } } @override bool get isActionEnabled { if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) { return false; } return state._value.selection.isValid; } } class _WebComposingDisablingCallbackAction extends CallbackAction { _WebComposingDisablingCallbackAction(this.state, {required super.onInvoke}); final EditableTextState state; @override bool get isActionEnabled { if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) { return false; } return super.isActionEnabled; } } class _SelectAllAction extends ContextAction { _SelectAllAction(this.state); final EditableTextState state; @override Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { if (!state.widget.selectionEnabled) { return null; } return Actions.invoke( context!, UpdateSelectionIntent( state._value, TextSelection(baseOffset: 0, extentOffset: state._value.text.length), intent.cause, ), ); } } class _CopySelectionAction extends ContextAction { _CopySelectionAction(this.state); final EditableTextState state; @override void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { if (!state._value.selection.isValid || state._value.selection.isCollapsed) { return; } if (!state.widget.selectionEnabled) { return; } if (intent.collapseSelection) { state.cutSelection(intent.cause); } else { state.copySelection(intent.cause); } } } class _PasteSelectionAction extends ContextAction { _PasteSelectionAction(this.state); final EditableTextState state; @override void invoke(PasteTextIntent intent, [BuildContext? context]) { if (!state.widget.selectionEnabled) { return; } state.pasteText(intent.cause); } } /// A [ClipboardStatusNotifier] whose [value] is hardcoded to /// [ClipboardStatus.pasteable]. /// /// Useful to avoid showing a permission dialog on web, which happens when /// [Clipboard.hasStrings] is called. class _WebClipboardStatusNotifier extends ClipboardStatusNotifier { @override ClipboardStatus value = ClipboardStatus.pasteable; @override Future update() { return Future.value(); } } class _EditableTextTapOutsideAction extends ContextAction { _EditableTextTapOutsideAction(); @override void invoke(EditableTextTapOutsideIntent intent, [BuildContext? context]) { // The focus dropping behavior is only present on desktop platforms. switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: case TargetPlatform.fuchsia: // On mobile platforms, we don't unfocus on touch events unless they're // in the web browser, but we do unfocus for all other kinds of events. switch (intent.pointerDownEvent.kind) { case ui.PointerDeviceKind.touch: if (kIsWeb) { intent.focusNode.unfocus(); } case ui.PointerDeviceKind.mouse: case ui.PointerDeviceKind.stylus: case ui.PointerDeviceKind.invertedStylus: case ui.PointerDeviceKind.unknown: intent.focusNode.unfocus(); case ui.PointerDeviceKind.trackpad: throw UnimplementedError( 'Unexpected pointer down event for trackpad', ); } case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: intent.focusNode.unfocus(); } } } class _EditableTextTapUpOutsideAction extends ContextAction { _EditableTextTapUpOutsideAction(); @override void invoke(EditableTextTapUpOutsideIntent intent, [BuildContext? context]) { // The default action is a no-op. } } /// A utility class for overriding the text styles of a [TextSpan] tree. // When changes are made to this class, the equivalent API in text.dart // must also be updated. // TODO(Renzo-Olivares): Remove after investigating a solution for overriding all // styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952. class _OverridingTextStyleTextSpanUtils { static TextSpan applyTextSpacingOverrides({ double? lineHeightScaleFactor, double? letterSpacing, double? wordSpacing, required TextSpan textSpan, }) { if (lineHeightScaleFactor == null && letterSpacing == null && wordSpacing == null) { return textSpan; } return _applyTextStyleOverrides( TextStyle( height: lineHeightScaleFactor, letterSpacing: letterSpacing, wordSpacing: wordSpacing, ), textSpan, ); } static TextSpan _applyTextStyleOverrides( TextStyle overrideTextStyle, TextSpan textSpan, ) { return TextSpan( text: textSpan.text, children: textSpan.children?.map((InlineSpan child) { if (child is TextSpan && child.runtimeType == TextSpan) { return _applyTextStyleOverrides(overrideTextStyle, child); } return child; }).toList(), style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle, recognizer: textSpan.recognizer, mouseCursor: textSpan.mouseCursor, onEnter: textSpan.onEnter, onExit: textSpan.onExit, semanticsLabel: textSpan.semanticsLabel, semanticsIdentifier: textSpan.semanticsIdentifier, locale: textSpan.locale, spellOut: textSpan.spellOut, ); } } ================================================ FILE: lib/common/widgets/flutter/text_field/spell_check.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'editable_text.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart' show EditableTextContextMenuBuilder; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/services.dart' show SpellCheckService; /// Controls how spell check is performed for text input. /// /// This configuration determines the [SpellCheckService] used to fetch the /// [List] spell check results and the [TextStyle] used to /// mark misspelled words within text input. @immutable class SpellCheckConfiguration { /// Creates a configuration that specifies the service and suggestions handler /// for spell check. const SpellCheckConfiguration({ this.spellCheckService, this.misspelledSelectionColor, this.misspelledTextStyle, this.spellCheckSuggestionsToolbarBuilder, }) : _spellCheckEnabled = true; /// Creates a configuration that disables spell check. const SpellCheckConfiguration.disabled() : _spellCheckEnabled = false, spellCheckService = null, spellCheckSuggestionsToolbarBuilder = null, misspelledTextStyle = null, misspelledSelectionColor = null; /// The service used to fetch spell check results for text input. final SpellCheckService? spellCheckService; /// The color the paint the selection highlight when spell check is showing /// suggestions for a misspelled word. /// /// For example, on iOS, the selection appears red while the spell check menu /// is showing. final Color? misspelledSelectionColor; /// Style used to indicate misspelled words. /// /// This is nullable to allow style-specific wrappers of [EditableText] /// to infer this, but this must be specified if this configuration is /// provided directly to [EditableText] or its construction will fail with an /// assertion error. final TextStyle? misspelledTextStyle; /// Builds the toolbar used to display spell check suggestions for misspelled /// words. final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder; final bool _spellCheckEnabled; /// Whether or not the configuration should enable or disable spell check. bool get spellCheckEnabled => _spellCheckEnabled; /// Returns a copy of the current [SpellCheckConfiguration] instance with /// specified overrides. SpellCheckConfiguration copyWith({ SpellCheckService? spellCheckService, Color? misspelledSelectionColor, TextStyle? misspelledTextStyle, EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder, }) { if (!_spellCheckEnabled) { // A new configuration should be constructed to enable spell check. return const SpellCheckConfiguration.disabled(); } return SpellCheckConfiguration( spellCheckService: spellCheckService ?? this.spellCheckService, misspelledSelectionColor: misspelledSelectionColor ?? this.misspelledSelectionColor, misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle, spellCheckSuggestionsToolbarBuilder: spellCheckSuggestionsToolbarBuilder ?? this.spellCheckSuggestionsToolbarBuilder, ); } @override String toString() { return '${objectRuntimeType(this, 'SpellCheckConfiguration')}(' '${_spellCheckEnabled ? 'enabled' : 'disabled'}, ' 'service: $spellCheckService, ' 'text style: $misspelledTextStyle, ' 'toolbar builder: $spellCheckSuggestionsToolbarBuilder' ')'; } @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } return other is SpellCheckConfiguration && other.spellCheckService == spellCheckService && other.misspelledTextStyle == misspelledTextStyle && other.spellCheckSuggestionsToolbarBuilder == spellCheckSuggestionsToolbarBuilder && other._spellCheckEnabled == _spellCheckEnabled; } @override int get hashCode => Object.hash( spellCheckService, misspelledTextStyle, spellCheckSuggestionsToolbarBuilder, _spellCheckEnabled, ); } ================================================ FILE: lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; import 'package:flutter/material.dart' hide EditableText, EditableTextState, AdaptiveTextSelectionToolbar; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan; // The default height of the SpellCheckSuggestionsToolbar, which // assumes there are the maximum number of spell check suggestions available, 3. // Size eyeballed on Pixel 4 emulator running Android API 31. const double _kDefaultToolbarHeight = 193.0; /// The maximum number of suggestions in the toolbar is 3, plus a delete button. const int _kMaxSuggestions = 3; /// The default spell check suggestions toolbar for Android. /// /// Tries to position itself below the [anchor], but if it doesn't fit, then it /// readjusts to fit above bottom view insets. /// /// See also: /// /// * [CupertinoSpellCheckSuggestionsToolbar], which is similar but builds an /// iOS-style spell check toolbar. class SpellCheckSuggestionsToolbar extends StatelessWidget { /// Constructs a [SpellCheckSuggestionsToolbar]. /// /// [buttonItems] must not contain more than four items, generally three /// suggestions and one delete button. const SpellCheckSuggestionsToolbar({ super.key, required this.anchor, required this.buttonItems, }) : assert(buttonItems.length <= _kMaxSuggestions + 1); /// Constructs a [SpellCheckSuggestionsToolbar] with the default children for /// an [EditableText]. /// /// See also: /// * [CupertinoSpellCheckSuggestionsToolbar.editableText], which is similar /// but builds an iOS-style toolbar. SpellCheckSuggestionsToolbar.editableText({ super.key, required EditableTextState editableTextState, }) : buttonItems = buildButtonItems(editableTextState) ?? [], anchor = getToolbarAnchor(editableTextState.contextMenuAnchors); /// {@template flutter.material.SpellCheckSuggestionsToolbar.anchor} /// The focal point below which the toolbar attempts to position itself. /// {@endtemplate} final Offset anchor; /// The [ContextMenuButtonItem]s that will be turned into the correct button /// widgets and displayed in the spell check suggestions toolbar. /// /// Must not contain more than four items, typically three suggestions and a /// delete button. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.buttonItems], the list of /// [ContextMenuButtonItem]s that are used to build the buttons of the /// text selection toolbar. /// * [CupertinoSpellCheckSuggestionsToolbar.buttonItems], the list of /// [ContextMenuButtonItem]s used to build the Cupertino style spell check /// suggestions toolbar. final List buttonItems; /// Builds the button items for the toolbar based on the available /// spell check suggestions. static List? buildButtonItems( EditableTextState editableTextState, ) { // Determine if composing region is misspelled. final SuggestionSpan? spanAtCursorIndex = editableTextState .findSuggestionSpanAtCursorIndex( editableTextState.currentTextEditingValue.selection.baseOffset, ); if (spanAtCursorIndex == null) { return null; } final buttonItems = []; // Build suggestion buttons. for (final String suggestion in spanAtCursorIndex.suggestions.take( _kMaxSuggestions, )) { buttonItems.add( ContextMenuButtonItem( onPressed: () { if (!editableTextState.mounted) { return; } _replaceText( editableTextState, suggestion, spanAtCursorIndex.range, ); }, label: suggestion, ), ); } // Build delete button. final deleteButton = ContextMenuButtonItem( onPressed: () { if (!editableTextState.mounted) { return; } _replaceText( editableTextState, '', editableTextState.currentTextEditingValue.composing, ); }, type: ContextMenuButtonType.delete, ); buttonItems.add(deleteButton); return buttonItems; } static void _replaceText( EditableTextState editableTextState, String text, TextRange replacementRange, ) { // Replacement cannot be performed if the text is read only or obscured. assert( !editableTextState.widget.readOnly && !editableTextState.widget.obscureText, ); final TextEditingValue newValue = editableTextState.textEditingValue .replaced( replacementRange, text, ); editableTextState.userUpdateTextEditingValue( newValue, SelectionChangedCause.toolbar, ); // Schedule a call to bringIntoView() after renderEditable updates. SchedulerBinding.instance.addPostFrameCallback((Duration duration) { if (editableTextState.mounted) { editableTextState.bringIntoView( editableTextState.textEditingValue.selection.extent, ); } }, debugLabel: 'SpellCheckerSuggestionsToolbar.bringIntoView'); editableTextState.hideToolbar(); } /// Determines the Offset that the toolbar will be anchored to. static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) { // Since this will be positioned below the anchor point, use the secondary // anchor by default. return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!; } /// Builds the toolbar buttons based on the [buttonItems]. List _buildToolbarButtons(BuildContext context) { return buttonItems.map((ContextMenuButtonItem buttonItem) { final button = TextSelectionToolbarTextButton( padding: const EdgeInsets.fromLTRB(20, 0, 0, 0), onPressed: buttonItem.onPressed, alignment: Alignment.centerLeft, child: Text( AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem), style: buttonItem.type == ContextMenuButtonType.delete ? const TextStyle(color: Colors.blue) : null, ), ); if (buttonItem.type != ContextMenuButtonType.delete) { return button; } return DecoratedBox( decoration: const BoxDecoration( border: Border(top: BorderSide(color: Colors.grey)), ), child: button, ); }).toList(); } @override Widget build(BuildContext context) { if (buttonItems.isEmpty) { return const SizedBox.shrink(); } // Adjust toolbar height if needed. final double spellCheckSuggestionsToolbarHeight = _kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length)); // Incorporate the padding distance between the content and toolbar. final MediaQueryData mediaQueryData = MediaQuery.of(context); final double softKeyboardViewInsetsBottom = mediaQueryData.viewInsets.bottom; final double paddingAbove = mediaQueryData.padding.top + CupertinoTextSelectionToolbar.kToolbarScreenPadding; // Makes up for the Padding. final localAdjustment = Offset( CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove, ); return Padding( padding: EdgeInsets.fromLTRB( CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove, CupertinoTextSelectionToolbar.kToolbarScreenPadding, CupertinoTextSelectionToolbar.kToolbarScreenPadding + softKeyboardViewInsetsBottom, ), child: CustomSingleChildLayout( delegate: SpellCheckSuggestionsToolbarLayoutDelegate( anchor: anchor - localAdjustment, ), child: AnimatedSize( // This duration was eyeballed on a Pixel 2 emulator running Android // API 28 for the Material TextSelectionToolbar. duration: const Duration(milliseconds: 140), child: _SpellCheckSuggestionsToolbarContainer( height: spellCheckSuggestionsToolbarHeight, children: [..._buildToolbarButtons(context)], ), ), ), ); } } /// The Material-styled toolbar outline for the spell check suggestions /// toolbar. class _SpellCheckSuggestionsToolbarContainer extends StatelessWidget { const _SpellCheckSuggestionsToolbarContainer({ required this.height, required this.children, }); final double height; final List children; @override Widget build(BuildContext context) { return Material( // This elevation was eyeballed on a Pixel 4 emulator running Android // API 31 for the SpellCheckSuggestionsToolbar. elevation: 2.0, type: MaterialType.card, child: SizedBox( // This width was eyeballed on a Pixel 4 emulator running Android // API 31 for the SpellCheckSuggestionsToolbar. width: 165.0, height: height, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: children, ), ), ); } } ================================================ FILE: lib/common/widgets/flutter/text_field/system_context_menu.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/material.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide EditableText, EditableTextState; import 'package:flutter/services.dart'; /// Displays the system context menu on top of the Flutter view. /// /// Currently, only supports iOS 16.0 and above and displays nothing on other /// platforms. /// /// The context menu is the menu that appears, for example, when doing text /// selection. Flutter typically draws this menu itself, but this class deals /// with the platform-rendered context menu instead. /// /// There can only be one system context menu visible at a time. Building this /// widget when the system context menu is already visible will hide the old one /// and display this one. A system context menu that is hidden is informed via /// [onSystemHide]. /// /// Pass [items] to specify the buttons that will appear in the menu. Any items /// without a title will be given a default title from [WidgetsLocalizations]. /// /// By default, [items] will be set to the result of [getDefaultItems]. This /// method considers the state of the [EditableTextState] so that, for example, /// it will only include [IOSSystemContextMenuItemCopy] if there is currently a /// selection to copy. /// /// To check if the current device supports showing the system context menu, /// call [isSupported]. /// /// {@tool dartpad} /// This example shows how to create a [TextField] that uses the system context /// menu where supported and does not show a system notification when the user /// presses the "Paste" button. /// /// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart ** /// {@end-tool} /// /// See also: /// /// * [SystemContextMenuController], which directly controls the hiding and /// showing of the system context menu. class SystemContextMenu extends StatefulWidget { /// Creates an instance of [SystemContextMenu] that points to the given /// [anchor]. const SystemContextMenu._({ super.key, required this.anchor, required this.items, this.onSystemHide, }); /// Creates an instance of [SystemContextMenu] for the field indicated by the /// given [EditableTextState]. factory SystemContextMenu.editableText({ Key? key, required EditableTextState editableTextState, List? items, }) { final ( startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight, ) = editableTextState .getGlyphHeights(); return SystemContextMenu._( key: key, anchor: TextSelectionToolbarAnchors.getSelectionRect( editableTextState.renderEditable, startGlyphHeight, endGlyphHeight, editableTextState.renderEditable.getEndpointsForSelection( editableTextState.textEditingValue.selection, ), ), items: items ?? getDefaultItems(editableTextState), onSystemHide: () => editableTextState.hideToolbar(false), ); } /// The [Rect] that the context menu should point to. final Rect anchor; /// A list of the items to be displayed in the system context menu. /// /// When passed, items will be shown regardless of the state of text input. /// For example, [IOSSystemContextMenuItemCopy] will produce a copy button /// even when there is no selection to copy. Use [EditableTextState] and/or /// the result of [getDefaultItems] to add and remove items based on the state /// of the input. /// /// Defaults to the result of [getDefaultItems]. /// /// To add custom menu items, pass [IOSSystemContextMenuItemCustom] instances /// in the [items] list. Each custom item requires a title and an onPressed callback. /// /// See also: /// /// * [IOSSystemContextMenuItemCustom], which creates custom menu items. final List items; /// Called when the system hides this context menu. /// /// For example, tapping outside of the context menu typically causes the /// system to hide the menu. /// /// This is not called when showing a new system context menu causes another /// to be hidden. final VoidCallback? onSystemHide; /// Whether the current device supports showing the system context menu. /// /// Currently, this is only supported on newer versions of iOS. /// /// See also: /// /// * [isSupportedByField], which uses this method and determines whether an /// individual [EditableTextState] supports the system context menu. static bool isSupported(BuildContext context) { return defaultTargetPlatform == TargetPlatform.iOS && (MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false); } /// Whether the given field supports showing the system context menu. /// /// Currently [SystemContextMenu] is only supported with an active /// [TextInputConnection]. In cases where this isn't possible, such as in a /// read-only field, fall back to using a Flutter-rendered context menu like /// [AdaptiveTextSelectionToolbar]. /// /// See also: /// /// * [isSupported], which is used by this method and determines whether the /// platform in general supports showing the system context menu. static bool isSupportedByField(EditableTextState editableTextState) { return !editableTextState.widget.readOnly && isSupported(editableTextState.context); } /// The default [items] for the given [EditableTextState]. /// /// For example, [IOSSystemContextMenuItemCopy] will only be included when the /// field represented by the [EditableTextState] has a selection. /// /// See also: /// /// * [EditableTextState.contextMenuButtonItems], which provides the default /// [ContextMenuButtonItem]s for the Flutter-rendered context menu. static List getDefaultItems( EditableTextState editableTextState, ) { final items = []; // Use the generic Flutter-rendered context menu model as the single source of truth. for (final ContextMenuButtonItem button in editableTextState.contextMenuButtonItems) { switch (button.type) { case ContextMenuButtonType.copy: items.add(const IOSSystemContextMenuItemCopy()); case ContextMenuButtonType.cut: items.add(const IOSSystemContextMenuItemCut()); case ContextMenuButtonType.paste: items.add(const IOSSystemContextMenuItemPaste()); case ContextMenuButtonType.selectAll: items.add(const IOSSystemContextMenuItemSelectAll()); case ContextMenuButtonType.lookUp: items.add(const IOSSystemContextMenuItemLookUp()); case ContextMenuButtonType.searchWeb: items.add(const IOSSystemContextMenuItemSearchWeb()); case ContextMenuButtonType.share: items.add(const IOSSystemContextMenuItemShare()); case ContextMenuButtonType.liveTextInput: items.add(const IOSSystemContextMenuItemLiveText()); case ContextMenuButtonType.delete: // No native iOS system menu button for Delete — intentionally ignored. case ContextMenuButtonType.custom: // Custom items are provided explicitly via SystemContextMenu.items, // not via defaults. Intentionally ignore in default mapping. } } return items; } @override State createState() => _SystemContextMenuState(); } class _SystemContextMenuState extends State { late final SystemContextMenuController _systemContextMenuController; @override void initState() { super.initState(); _systemContextMenuController = SystemContextMenuController( onSystemHide: widget.onSystemHide, ); } @override void dispose() { _systemContextMenuController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { assert(SystemContextMenu.isSupported(context)); if (widget.items.isNotEmpty) { final WidgetsLocalizations localizations = WidgetsLocalizations.of( context, ); final List itemDatas = widget.items .map((IOSSystemContextMenuItem item) => item.getData(localizations)) .toList(); _systemContextMenuController.showWithItems(widget.anchor, itemDatas); } return const SizedBox.shrink(); } } ================================================ FILE: lib/common/widgets/flutter/text_field/text_field.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: uri_does_not_exist_in_doc_import /// @docImport 'input_border.dart'; /// @docImport 'material.dart'; /// @docImport 'scaffold.dart'; /// @docImport 'text_form_field.dart'; /// @docImport 'text_theme.dart'; library; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/text_field.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/system_context_menu.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState, EditableTextContextMenuBuilder, SystemContextMenu, CupertinoSpellCheckSuggestionsToolbar, SpellCheckConfiguration, CupertinoTextField, TextSelectionGestureDetectorBuilder, TextSelectionOverlay, TextSelectionGestureDetectorBuilderDelegate; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide EditableText, EditableTextState, EditableTextContextMenuBuilder, AdaptiveTextSelectionToolbar, SystemContextMenu, SpellCheckSuggestionsToolbar, SpellCheckConfiguration, TextSelectionGestureDetectorBuilder, TextSelectionOverlay, TextSelectionGestureDetectorBuilderDelegate; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { _TextFieldSelectionGestureDetectorBuilder({ required RichTextFieldState state, required super.controller, }) : _state = state, super(delegate: state); final RichTextFieldState _state; @override bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled; @override void onUserTap() { _state.widget.onTap?.call(); } } /// A Material Design text field. /// /// A text field lets the user enter text, either with hardware keyboard or with /// an onscreen keyboard. /// /// The text field calls the [onChanged] callback whenever the user changes the /// text in the field. If the user indicates that they are done typing in the /// field (e.g., by pressing a button on the soft keyboard), the text field /// calls the [onSubmitted] callback. /// /// To control the text that is displayed in the text field, use the /// [controller]. For example, to set the initial value of the text field, use /// a [controller] that already contains some text. The [controller] can also /// control the selection and composing region (and to observe changes to the /// text, selection, and composing region). /// /// By default, a text field has a [decoration] that draws a divider below the /// text field. You can use the [decoration] property to control the decoration, /// for example by adding a label or an icon. If you set the [decoration] /// property to null, the decoration will be removed entirely, including the /// extra padding introduced by the decoration to save space for the labels. /// /// If [decoration] is non-null (which is the default), the text field requires /// one of its ancestors to be a [Material] widget. /// /// To integrate the [RichTextField] into a [Form] with other [FormField] widgets, /// consider using [TextFormField]. /// /// {@template flutter.material.textfield.wantKeepAlive} /// When the widget has focus, it will prevent itself from disposing via its /// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in /// order to avoid losing the selection. Removing the focus will allow it to be /// disposed. /// {@endtemplate} /// /// Remember to call [RichTextEditingController.dispose] on the [RichTextEditingController] /// when it is no longer needed. This will ensure we discard any resources used /// by the object. /// /// If this field is part of a scrolling container that lazily constructs its /// children, like a [ListView] or a [CustomScrollView], then a [controller] /// should be specified. The controller's lifetime should be managed by a /// stateful widget ancestor of the scrolling container. /// /// ## Obscured Input /// /// {@tool dartpad} /// This example shows how to create a [RichTextField] that will obscure input. The /// [InputDecoration] surrounds the field in a border using [OutlineInputBorder] /// and adds a label. /// /// ** See code in examples/api/lib/material/text_field/text_field.0.dart ** /// {@end-tool} /// /// ## Reading values /// /// A common way to read a value from a TextField is to use the [onSubmitted] /// callback. This callback is applied to the text field's current value when /// the user finishes editing. /// /// {@tool dartpad} /// This sample shows how to get a value from a TextField via the [onSubmitted] /// callback. /// /// ** See code in examples/api/lib/material/text_field/text_field.1.dart ** /// {@end-tool} /// /// {@macro flutter.widgets.EditableText.lifeCycle} /// /// For most applications the [onSubmitted] callback will be sufficient for /// reacting to user input. /// /// The [onEditingComplete] callback also runs when the user finishes editing. /// It's different from [onSubmitted] because it has a default value which /// updates the text controller and yields the keyboard focus. Applications that /// require different behavior can override the default [onEditingComplete] /// callback. /// /// Keep in mind you can also always read the current string from a TextField's /// [RichTextEditingController] using [RichTextEditingController.text]. /// /// ## Handling emojis and other complex characters /// {@macro flutter.widgets.EditableText.onChanged} /// /// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦 /// into the field and submitting. Because the example code measures the length /// with `value.characters.length`, the emoji is correctly counted as a single /// character. /// /// {@macro flutter.widgets.editableText.showCaretOnScreen} /// /// {@macro flutter.widgets.editableText.accessibility} /// /// {@tool dartpad} /// This sample shows how to style a text field to match a filled or outlined /// Material Design 3 text field. /// /// ** See code in examples/api/lib/material/text_field/text_field.2.dart ** /// {@end-tool} /// /// ## Scrolling Considerations /// /// If this [RichTextField] is not a descendant of [Scaffold] and is being used /// within a [Scrollable] or nested [Scrollable]s, consider placing a /// [ScrollNotificationObserver] above the root [Scrollable] that contains this /// [RichTextField] to ensure proper scroll coordination for [RichTextField] and its /// components like [TextSelectionOverlay]. /// /// {@tool dartpad} /// This sample demonstrates how to use the [Shortcuts] and [Actions] widgets /// to create a custom `Shift+Enter` keyboard shortcut for inserting a new line /// in a [RichTextField]. /// /// ** See code in examples/api/lib/material/text_field/text_field.3.dart ** /// {@end-tool} /// /// See also: /// /// * [TextFormField], which integrates with the [Form] widget. /// * [InputDecorator], which shows the labels and other visual elements that /// surround the actual text editing widget. /// * [EditableText], which is the raw text editing control at the heart of a /// [RichTextField]. The [EditableText] widget is rarely used directly unless /// you are implementing an entirely different design language, such as /// Cupertino. /// * /// * Cookbook: [Create and style a text field](https://docs.flutter.dev/cookbook/forms/text-input) /// * Cookbook: [Handle changes to a text field](https://docs.flutter.dev/cookbook/forms/text-field-changes) /// * Cookbook: [Retrieve the value of a text field](https://docs.flutter.dev/cookbook/forms/retrieve-input) /// * Cookbook: [Focus and text fields](https://docs.flutter.dev/cookbook/forms/focus) class RichTextField extends StatefulWidget { /// Creates a Material Design text field. /// /// If [decoration] is non-null (which is the default), the text field requires /// one of its ancestors to be a [Material] widget. /// /// To remove the decoration entirely (including the extra padding introduced /// by the decoration to save space for the labels), set the [decoration] to /// null. /// /// The [maxLines] property can be set to null to remove the restriction on /// the number of lines. By default, it is one, meaning this is a single-line /// text field. [maxLines] must not be zero. /// /// The [maxLength] property is set to null by default, which means the /// number of characters allowed in the text field is not restricted. If /// [maxLength] is set a character counter will be displayed below the /// field showing how many characters have been entered. If the value is /// set to a positive integer it will also display the maximum allowed /// number of characters to be entered. If the value is set to /// [RichTextField.noMaxLength] then only the current length is displayed. /// /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to /// [MaxLengthEnforcement.none]. /// The text field enforces the length with a [LengthLimitingTextInputFormatter], /// which is evaluated after the supplied [inputFormatters], if any. /// The [maxLength] value must be either null or greater than zero. /// /// If [maxLengthEnforcement] is set to [MaxLengthEnforcement.none], then more /// than [maxLength] characters may be entered, and the error counter and /// divider will switch to the [decoration].errorStyle when the limit is /// exceeded. /// /// The text cursor is not shown if [showCursor] is false or if [showCursor] /// is null (the default) and [readOnly] is true. /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default /// to [EditableText.defaultSelectionHeightStyle] and /// [EditableText.defaultSelectionHeightStyle], respectively. /// /// See also: /// /// * [maxLength], which discusses the precise meaning of "number of /// characters" and how it may differ from the intuitive meaning. const RichTextField({ super.key, this.groupId = EditableText, required this.controller, this.focusNode, this.decoration = const InputDecoration(), TextInputType? keyboardType, this.textInputAction, this.textCapitalization = TextCapitalization.none, this.style, this.strutStyle, this.textAlign = TextAlign.start, this.textAlignVertical, this.textDirection, this.readOnly = false, @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.toolbarOptions, this.showCursor, this.autofocus = false, this.statesController, this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, this.maxLines = 1, this.minLines, this.expands = false, this.maxLength, this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, this.onAppPrivateCommand, this.inputFormatters, this.enabled, this.ignorePointers, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, this.cursorOpacityAnimates, this.cursorColor, this.cursorErrorColor, this.selectionHeightStyle, this.selectionWidthStyle, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, this.selectAllOnFocus, this.selectionControls, this.onTap, this.onTapAlwaysCalled = false, this.onTapOutside, this.onTapUpOutside, this.mouseCursor, this.buildCounter, this.scrollController, this.scrollPhysics, this.autofillHints = const [], this.contentInsertionConfiguration, this.clipBehavior = Clip.hardEdge, this.restorationId, @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) this.scribbleEnabled = true, this.stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, this.contextMenuBuilder = _defaultContextMenuBuilder, this.canRequestFocus = true, this.spellCheckConfiguration, this.magnifierConfiguration, this.hintLocales, }) : assert(obscuringCharacter.length == 1), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), assert( !expands || (maxLines == null && minLines == null), 'minLines and maxLines must be null when expands is true.', ), assert( !obscureText || maxLines == 1, 'Obscured fields cannot be multiline.', ), assert( maxLength == null || maxLength == RichTextField.noMaxLength || maxLength > 0, ), // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set. assert( !identical(textInputAction, TextInputAction.newline) || maxLines == 1 || !identical(keyboardType, TextInputType.text), 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.', ), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText); /// The configuration for the magnifier of this text field. /// /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] /// on Android, and builds nothing on all other platforms. To suppress the /// magnifier, consider passing [TextMagnifierConfiguration.disabled]. /// /// {@macro flutter.widgets.magnifier.intro} /// /// {@tool dartpad} /// This sample demonstrates how to customize the magnifier that this text field uses. /// /// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart ** /// {@end-tool} final TextMagnifierConfiguration? magnifierConfiguration; /// {@macro flutter.widgets.editableText.groupId} final Object groupId; /// Controls the text being edited. /// /// If null, this widget will create its own [RichTextEditingController]. final RichTextEditingController controller; /// Defines the keyboard focus for this widget. /// /// The [focusNode] is a long-lived object that's typically managed by a /// [StatefulWidget] parent. See [FocusNode] for more information. /// /// To give the keyboard focus to this widget, provide a [focusNode] and then /// use the current [FocusScope] to request the focus: /// /// ```dart /// FocusScope.of(context).requestFocus(myFocusNode); /// ``` /// /// This happens automatically when the widget is tapped. /// /// To be notified when the widget gains or loses the focus, add a listener /// to the [focusNode]: /// /// ```dart /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); /// ``` /// /// If null, this widget will create its own [FocusNode]. /// /// ## Keyboard /// /// Requesting the focus will typically cause the keyboard to be shown /// if it's not showing already. /// /// On Android, the user can hide the keyboard - without changing the focus - /// with the system back button. They can restore the keyboard's visibility /// by tapping on a text field. The user might hide the keyboard and /// switch to a physical keyboard, or they might just need to get it /// out of the way for a moment, to expose something it's /// obscuring. In this case requesting the focus again will not /// cause the focus to change, and will not make the keyboard visible. /// /// This widget builds an [EditableText] and will ensure that the keyboard is /// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. final FocusNode? focusNode; /// The decoration to show around the text field. /// /// By default, draws a horizontal line under the text field but can be /// configured to show an icon, label, hint text, and error text. /// /// Specify null to remove the decoration entirely (including the /// extra padding introduced by the decoration to save space for the labels). final InputDecoration? decoration; /// {@macro flutter.widgets.editableText.keyboardType} final TextInputType keyboardType; /// {@template flutter.widgets.TextField.textInputAction} /// The type of action button to use for the keyboard. /// /// Defaults to [TextInputAction.newline] if [keyboardType] is /// [TextInputType.multiline] and [TextInputAction.done] otherwise. /// {@endtemplate} final TextInputAction? textInputAction; /// {@macro flutter.widgets.editableText.textCapitalization} final TextCapitalization textCapitalization; /// The style to use for the text being edited. /// /// This text style is also used as the base style for the [decoration]. /// /// If null, [TextTheme.bodyLarge] will be used. When the text field is disabled, /// [TextTheme.bodyLarge] with an opacity of 0.38 will be used instead. /// /// If null and [ThemeData.useMaterial3] is false, [TextTheme.titleMedium] will /// be used. When the text field is disabled, [TextTheme.titleMedium] with /// [ThemeData.disabledColor] will be used instead. final TextStyle? style; /// {@macro flutter.widgets.editableText.strutStyle} final StrutStyle? strutStyle; /// {@macro flutter.widgets.editableText.textAlign} final TextAlign textAlign; /// {@macro flutter.material.InputDecorator.textAlignVertical} final TextAlignVertical? textAlignVertical; /// {@macro flutter.widgets.editableText.textDirection} final TextDirection? textDirection; /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; /// Represents the interactive "state" of this widget in terms of a set of /// [WidgetState]s, including [WidgetState.disabled], [WidgetState.hovered], /// [WidgetState.error], and [WidgetState.focused]. /// /// Classes based on this one can provide their own /// [WidgetStatesController] to which they've added listeners. /// They can also update the controller's [WidgetStatesController.value] /// however, this may only be done when it's safe to call /// [State.setState], like in an event handler. /// /// The controller's [WidgetStatesController.value] represents the set of /// states that a widget's visual properties, typically [WidgetStateProperty] /// values, are resolved against. It is _not_ the intrinsic state of the widget. /// The widget is responsible for ensuring that the controller's /// [WidgetStatesController.value] tracks its intrinsic state. For example /// one cannot request the keyboard focus for a widget by adding [WidgetState.focused] /// to its controller. When the widget gains the or loses the focus it will /// [WidgetStatesController.update] its controller's [WidgetStatesController.value] /// and notify listeners of the change. final WidgetStatesController? statesController; /// {@macro flutter.widgets.editableText.obscuringCharacter} final String obscuringCharacter; /// {@macro flutter.widgets.editableText.obscureText} final bool obscureText; /// {@macro flutter.widgets.editableText.autocorrect} final bool? autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} final SmartQuotesType smartQuotesType; /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} final bool enableSuggestions; /// {@macro flutter.widgets.editableText.maxLines} /// * [expands], which determines whether the field should fill the height of /// its parent. final int? maxLines; /// {@macro flutter.widgets.editableText.minLines} /// * [expands], which determines whether the field should fill the height of /// its parent. final int? minLines; /// {@macro flutter.widgets.editableText.expands} final bool expands; /// {@macro flutter.widgets.editableText.readOnly} final bool readOnly; /// Configuration of toolbar options. /// /// If not set, select all and paste will default to be enabled. Copy and cut /// will be disabled if [obscureText] is true. If [readOnly] is true, /// paste and cut will be disabled regardless. @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) final ToolbarOptions? toolbarOptions; /// {@macro flutter.widgets.editableText.showCursor} final bool? showCursor; /// If [maxLength] is set to this value, only the "current input length" /// part of the character counter is shown. static const int noMaxLength = -1; /// The maximum number of characters (Unicode grapheme clusters) to allow in /// the text field. /// /// If set, a character counter will be displayed below the /// field showing how many characters have been entered. If set to a number /// greater than 0, it will also display the maximum number allowed. If set /// to [RichTextField.noMaxLength] then only the current character count is displayed. /// To remove the counter, set [InputDecoration.counterText] to an empty string or /// return null from [RichTextField.buildCounter] callback. /// /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to /// [MaxLengthEnforcement.none]. /// /// The text field enforces the length with a [LengthLimitingTextInputFormatter], /// which is evaluated after the supplied [inputFormatters], if any. /// /// This value must be either null, [RichTextField.noMaxLength], or greater than 0. /// If null (the default) then there is no limit to the number of characters /// that can be entered. If set to [RichTextField.noMaxLength], then no limit will /// be enforced, but the number of characters entered will still be displayed. /// /// Whitespace characters (e.g. newline, space, tab) are included in the /// character count. /// /// If [maxLengthEnforcement] is [MaxLengthEnforcement.none], then more than /// [maxLength] characters may be entered, but the error counter and divider /// will switch to the [decoration]'s [InputDecoration.errorStyle] when the /// limit is exceeded. /// /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} final int? maxLength; /// Determines how the [maxLength] limit should be enforced. /// /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} /// /// {@macro flutter.services.textFormatter.maxLengthEnforcement} final MaxLengthEnforcement? maxLengthEnforcement; /// {@macro flutter.widgets.editableText.onChanged} /// /// See also: /// /// * [inputFormatters], which are called before [onChanged] /// runs and can validate and change ("format") the input value. /// * [onEditingComplete], [onSubmitted]: /// which are more specialized input change notifications. final ValueChanged? onChanged; /// {@macro flutter.widgets.editableText.onEditingComplete} final VoidCallback? onEditingComplete; /// {@macro flutter.widgets.editableText.onSubmitted} /// /// See also: /// /// * [TextInputAction.next] and [TextInputAction.previous], which /// automatically shift the focus to the next/previous focusable item when /// the user is done editing. final ValueChanged? onSubmitted; /// {@macro flutter.widgets.editableText.onAppPrivateCommand} final AppPrivateCommandCallback? onAppPrivateCommand; /// {@macro flutter.widgets.editableText.inputFormatters} final List? inputFormatters; /// If false the text field is "disabled": it ignores taps and its /// [decoration] is rendered in grey. /// /// If non-null this property overrides the [decoration]'s /// [InputDecoration.enabled] property. /// /// When a text field is disabled, all of its children widgets are also /// disabled, including the [InputDecoration.suffixIcon]. If you need to keep /// the suffix icon interactive while disabling the text field, consider using /// [readOnly] and [enableInteractiveSelection] instead: /// /// ```dart /// TextField( /// enabled: true, /// readOnly: true, /// enableInteractiveSelection: false, /// decoration: InputDecoration( /// suffixIcon: IconButton( /// onPressed: () { /// // This will work because the TextField is enabled /// }, /// icon: const Icon(Icons.edit_outlined), /// ), /// ), /// ) /// ``` final bool? enabled; /// Determines whether this widget ignores pointer events. /// /// Defaults to null, and when null, does nothing. final bool? ignorePointers; /// {@macro flutter.widgets.editableText.cursorWidth} final double cursorWidth; /// {@macro flutter.widgets.editableText.cursorHeight} final double? cursorHeight; /// {@macro flutter.widgets.editableText.cursorRadius} final Radius? cursorRadius; /// {@macro flutter.widgets.editableText.cursorOpacityAnimates} final bool? cursorOpacityAnimates; /// The color of the cursor. /// /// The cursor indicates the current location of text insertion point in /// the field. /// /// If this is null it will default to the ambient /// [DefaultSelectionStyle.cursorColor]. If that is null, and the /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS] /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use /// the value of [ColorScheme.primary] of [ThemeData.colorScheme]. final Color? cursorColor; /// The color of the cursor when the [InputDecorator] is showing an error. /// /// If this is null it will default to [TextStyle.color] of /// [InputDecoration.errorStyle]. If that is null, it will use /// [ColorScheme.error] of [ThemeData.colorScheme]. final Color? cursorErrorColor; /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. final ui.BoxHeightStyle? selectionHeightStyle; /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. final ui.BoxWidthStyle? selectionWidthStyle; /// The appearance of the keyboard. /// /// This setting is only honored on iOS devices. /// /// If unset, defaults to [ThemeData.brightness]. final Brightness? keyboardAppearance; /// {@macro flutter.widgets.editableText.scrollPadding} final EdgeInsets scrollPadding; /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; /// {@macro flutter.widgets.editableText.selectAllOnFocus} final bool? selectAllOnFocus; /// {@macro flutter.widgets.editableText.selectionControls} final TextSelectionControls? selectionControls; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.editableText.selectionEnabled} bool get selectionEnabled => enableInteractiveSelection; /// {@template flutter.material.textfield.onTap} /// Called for the first tap in a series of taps. /// /// The text field builds a [GestureDetector] to handle input events like tap, /// to trigger focus requests, to move the caret, adjust the selection, etc. /// Handling some of those events by wrapping the text field with a competing /// GestureDetector is problematic. /// /// To unconditionally handle taps, without interfering with the text field's /// internal gesture detector, provide this callback. /// /// If the text field is created with [enabled] false, taps will not be /// recognized. /// /// To be notified when the text field gains or loses the focus, provide a /// [focusNode] and add a listener to that. /// /// To listen to arbitrary pointer events without competing with the /// text field's internal gesture detector, use a [Listener]. /// {@endtemplate} /// /// If [onTapAlwaysCalled] is enabled, this will also be called for consecutive /// taps. final GestureTapCallback? onTap; /// Whether [onTap] should be called for every tap. /// /// Defaults to false, so [onTap] is only called for each distinct tap. When /// enabled, [onTap] is called for every tap including consecutive taps. final bool onTapAlwaysCalled; /// {@macro flutter.widgets.editableText.onTapOutside} /// /// {@tool dartpad} /// This example shows how to use a `TextFieldTapRegion` to wrap a set of /// "spinner" buttons that increment and decrement a value in the [RichTextField] /// without causing the text field to lose keyboard focus. /// /// This example includes a generic `SpinnerField` class that you can copy /// into your own project and customize. /// /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart ** /// {@end-tool} /// /// See also: /// /// * [TapRegion] for how the region group is determined. final TapRegionCallback? onTapOutside; /// {@macro flutter.widgets.editableText.onTapUpOutside} final TapRegionUpCallback? onTapUpOutside; /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// /// If [mouseCursor] is a [WidgetStateMouseCursor], /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// /// * [WidgetState.error]. /// * [WidgetState.hovered]. /// * [WidgetState.focused]. /// * [WidgetState.disabled]. /// /// If this property is null, [WidgetStateMouseCursor.textable] will be used. /// /// The [mouseCursor] is the only property of [RichTextField] that controls the /// appearance of the mouse pointer. All other properties related to "cursor" /// stand for the text cursor, which is usually a blinking vertical line at /// the editing position. final MouseCursor? mouseCursor; /// Callback that generates a custom [InputDecoration.counter] widget. /// /// See [InputCounterWidgetBuilder] for an explanation of the passed in /// arguments. The returned widget will be placed below the line in place of /// the default widget built when [InputDecoration.counterText] is specified. /// /// The returned widget will be wrapped in a [Semantics] widget for /// accessibility, but it also needs to be accessible itself. For example, /// if returning a Text widget, set the [Text.semanticsLabel] property. /// /// {@tool snippet} /// ```dart /// Widget counter( /// BuildContext context, /// { /// required int currentLength, /// required int? maxLength, /// required bool isFocused, /// } /// ) { /// return Text( /// '$currentLength of $maxLength characters', /// semanticsLabel: 'character count', /// ); /// } /// ``` /// {@end-tool} /// /// If buildCounter returns null, then no counter and no Semantics widget will /// be created at all. final InputCounterWidgetBuilder? buildCounter; /// {@macro flutter.widgets.editableText.scrollPhysics} final ScrollPhysics? scrollPhysics; /// {@macro flutter.widgets.editableText.scrollController} final ScrollController? scrollController; /// {@macro flutter.widgets.editableText.autofillHints} /// {@macro flutter.services.AutofillConfiguration.autofillHints} final Iterable? autofillHints; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// {@template flutter.material.textfield.restorationId} /// Restoration ID to save and restore the state of the text field. /// /// If non-null, the text field will persist and restore its current scroll /// offset and - if no [controller] has been provided - the content of the /// text field. If a [controller] has been provided, it is the responsibility /// of the owner of that controller to persist and restore it, e.g. by using /// a [RestorableRichTextEditingController]. /// /// The state of this widget is persisted in a [RestorationBucket] claimed /// from the surrounding [RestorationScope] using the provided restoration ID. /// /// See also: /// /// * [RestorationManager], which explains how state restoration works in /// Flutter. /// {@endtemplate} final String? restorationId; /// {@macro flutter.widgets.editableText.scribbleEnabled} @Deprecated( 'Use `stylusHandwritingEnabled` instead. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) final bool scribbleEnabled; /// {@macro flutter.widgets.editableText.stylusHandwritingEnabled} final bool stylusHandwritingEnabled; /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} final ContentInsertionConfiguration? contentInsertionConfiguration; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} /// /// If not provided, will build a default menu based on the platform. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar], which is built by default. /// * [BrowserContextMenu], which allows the browser's context menu on web to /// be disabled and Flutter-rendered context menus to appear. final EditableTextContextMenuBuilder? contextMenuBuilder; /// Determine whether this text field can request the primary focus. /// /// Defaults to true. If false, the text field will not request focus /// when tapped, or when its context menu is displayed. If false it will not /// be possible to move the focus to the text field with tab key. final bool canRequestFocus; /// {@macro flutter.services.TextInputConfiguration.hintLocales} final List? hintLocales; static Widget _defaultContextMenuBuilder( BuildContext context, EditableTextState editableTextState, ) { if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText( editableTextState: editableTextState, ); } return AdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, ); } /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} /// /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this /// configuration, then [materialMisspelledTextStyle] is used by default. final SpellCheckConfiguration? spellCheckConfiguration; /// The [TextStyle] used to indicate misspelled words in the Material style. /// /// See also: /// * [SpellCheckConfiguration.misspelledTextStyle], the style configured to /// mark misspelled words with. /// * [CupertinoRichTextField.cupertinoMisspelledTextStyle], the style configured /// to mark misspelled words with in the Cupertino style. static const TextStyle materialMisspelledTextStyle = TextStyle( decoration: TextDecoration.underline, decorationColor: Colors.red, decorationStyle: TextDecorationStyle.wavy, ); /// Default builder for [RichTextField]'s spell check suggestions toolbar. /// /// On Apple platforms, builds an iOS-style toolbar. Everywhere else, builds /// an Android-style toolbar. /// /// See also: /// * [spellCheckConfiguration], where this is typically specified for /// [RichTextField]. /// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the /// parameter for which this is the default value for [RichTextField]. /// * [CupertinoRichTextField.defaultSpellCheckSuggestionsToolbarBuilder], which /// is like this but specifies the default for [CupertinoRichTextField]. @visibleForTesting static Widget defaultSpellCheckSuggestionsToolbarBuilder( BuildContext context, EditableTextState editableTextState, ) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: return CupertinoSpellCheckSuggestionsToolbar.editableText( editableTextState: editableTextState, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return SpellCheckSuggestionsToolbar.editableText( editableTextState: editableTextState, ); } } /// Returns a new [SpellCheckConfiguration] where the given configuration has /// had any missing values replaced with their defaults for the Android /// platform. static SpellCheckConfiguration inferAndroidSpellCheckConfiguration( SpellCheckConfiguration? configuration, ) { if (configuration == null || configuration == const SpellCheckConfiguration.disabled()) { return const SpellCheckConfiguration.disabled(); } return configuration.copyWith( misspelledTextStyle: configuration.misspelledTextStyle ?? RichTextField.materialMisspelledTextStyle, spellCheckSuggestionsToolbarBuilder: configuration.spellCheckSuggestionsToolbarBuilder ?? RichTextField.defaultSpellCheckSuggestionsToolbarBuilder, ); } @override State createState() => RichTextFieldState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add( DiagnosticsProperty( 'controller', controller, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'focusNode', focusNode, defaultValue: null, ), ) ..add( DiagnosticsProperty('enabled', enabled, defaultValue: null), ) ..add( DiagnosticsProperty( 'decoration', decoration, defaultValue: const InputDecoration(), ), ) ..add( DiagnosticsProperty( 'keyboardType', keyboardType, defaultValue: TextInputType.text, ), ) ..add( DiagnosticsProperty('style', style, defaultValue: null), ) ..add( DiagnosticsProperty('autofocus', autofocus, defaultValue: false), ) ..add( DiagnosticsProperty( 'obscuringCharacter', obscuringCharacter, defaultValue: '•', ), ) ..add( DiagnosticsProperty( 'obscureText', obscureText, defaultValue: false, ), ) ..add( DiagnosticsProperty( 'autocorrect', autocorrect, defaultValue: null, ), ) ..add( EnumProperty( 'smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, ), ) ..add( EnumProperty( 'smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, ), ) ..add( DiagnosticsProperty( 'enableSuggestions', enableSuggestions, defaultValue: true, ), ) ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) ..add(IntProperty('minLines', minLines, defaultValue: null)) ..add( DiagnosticsProperty('expands', expands, defaultValue: false), ) ..add(IntProperty('maxLength', maxLength, defaultValue: null)) ..add( EnumProperty( 'maxLengthEnforcement', maxLengthEnforcement, defaultValue: null, ), ) ..add( EnumProperty( 'textInputAction', textInputAction, defaultValue: null, ), ) ..add( EnumProperty( 'textCapitalization', textCapitalization, defaultValue: TextCapitalization.none, ), ) ..add( EnumProperty( 'textAlign', textAlign, defaultValue: TextAlign.start, ), ) ..add( DiagnosticsProperty( 'textAlignVertical', textAlignVertical, defaultValue: null, ), ) ..add( EnumProperty( 'textDirection', textDirection, defaultValue: null, ), ) ..add( DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), ) ..add( DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), ) ..add( DiagnosticsProperty( 'cursorRadius', cursorRadius, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'cursorOpacityAnimates', cursorOpacityAnimates, defaultValue: null, ), ) ..add( ColorProperty('cursorColor', cursorColor, defaultValue: null), ) ..add( ColorProperty('cursorErrorColor', cursorErrorColor, defaultValue: null), ) ..add( DiagnosticsProperty( 'keyboardAppearance', keyboardAppearance, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0), ), ) ..add( FlagProperty( 'selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled', ), ) ..add( DiagnosticsProperty( 'selectionControls', selectionControls, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollController', scrollController, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'scrollPhysics', scrollPhysics, defaultValue: null, ), ) ..add( DiagnosticsProperty( 'clipBehavior', clipBehavior, defaultValue: Clip.hardEdge, ), ) ..add( DiagnosticsProperty( 'scribbleEnabled', scribbleEnabled, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'stylusHandwritingEnabled', stylusHandwritingEnabled, defaultValue: EditableText.defaultStylusHandwritingEnabled, ), ) ..add( DiagnosticsProperty( 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true, ), ) ..add( DiagnosticsProperty( 'spellCheckConfiguration', spellCheckConfiguration, defaultValue: null, ), ) ..add( DiagnosticsProperty>( 'contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const [], defaultValue: contentInsertionConfiguration == null ? const [] : kDefaultContentInsertionMimeTypes, ), ) ..add( DiagnosticsProperty?>( 'hintLocales', hintLocales, defaultValue: null, ), ); } } class RichTextFieldState extends State with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { RichTextEditingController get _effectiveController => widget.controller; FocusNode? _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement ?? LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement( Theme.of(context).platform, ); bool _isHovering = false; bool get needsCounter => widget.maxLength != null && widget.decoration != null && widget.decoration!.counterText == null; bool _showSelectionHandles = false; late _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; // API for TextSelectionGestureDetectorBuilderDelegate. @override late bool forcePressEnabled; @override final GlobalKey editableTextKey = GlobalKey(); @override bool get selectionEnabled => widget.selectionEnabled && _isEnabled; // End of API for TextSelectionGestureDetectorBuilderDelegate. bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true; int get _currentLength => _effectiveController.value.text.characters.length; bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!; bool get _hasError => widget.decoration?.errorText != null || widget.decoration?.error != null || _hasIntrinsicError; Color get _errorColor => widget.cursorErrorColor ?? _getEffectiveDecoration().errorStyle?.color ?? Theme.of(context).colorScheme.error; InputDecoration _getEffectiveDecoration() { final MaterialLocalizations localizations = MaterialLocalizations.of( context, ); final ThemeData themeData = Theme.of(context); final InputDecorationThemeData decorationTheme = InputDecorationTheme.of( context, ); final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration()) .applyDefaults(decorationTheme) .copyWith( enabled: _isEnabled, hintMaxLines: widget.decoration?.hintMaxLines ?? decorationTheme.hintMaxLines ?? widget.maxLines, ); // No need to build anything if counter or counterText were given directly. if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null) { return effectiveDecoration; } // If buildCounter was provided, use it to generate a counter widget. Widget? counter; final int currentLength = _currentLength; if (effectiveDecoration.counter == null && effectiveDecoration.counterText == null && widget.buildCounter != null) { final bool isFocused = _effectiveFocusNode.hasFocus; final Widget? builtCounter = widget.buildCounter!( context, currentLength: currentLength, maxLength: widget.maxLength, isFocused: isFocused, ); // If buildCounter returns null, don't add a counter widget to the field. if (builtCounter != null) { counter = Semantics( container: true, liveRegion: isFocused, child: builtCounter, ); } return effectiveDecoration.copyWith(counter: counter); } if (widget.maxLength == null) { return effectiveDecoration; } // No counter widget var counterText = '$currentLength'; var semanticCounterText = ''; // Handle a real maxLength (positive number) if (widget.maxLength! > 0) { // Show the maxLength in the counter counterText += '/${widget.maxLength}'; final int remaining = (widget.maxLength! - currentLength).clamp( 0, widget.maxLength!, ); semanticCounterText = localizations.remainingTextFieldCharacterCount( remaining, ); } if (_hasIntrinsicError) { return effectiveDecoration.copyWith( errorText: effectiveDecoration.errorText ?? '', counterStyle: effectiveDecoration.errorStyle ?? (themeData.useMaterial3 ? _m3CounterErrorStyle(context) : _m2CounterErrorStyle(context)), counterText: counterText, semanticCounterText: semanticCounterText, ); } return effectiveDecoration.copyWith( counterText: counterText, semanticCounterText: semanticCounterText, ); } @override void initState() { super.initState(); _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder( state: this, controller: widget.controller, ); _effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled; _effectiveFocusNode.addListener(_handleFocusChanged); _initStatesController(); } bool get _canRequestFocus { final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; return switch (mode) { NavigationMode.traditional => widget.canRequestFocus && _isEnabled, NavigationMode.directional => true, }; } @override void didChangeDependencies() { super.didChangeDependencies(); _effectiveFocusNode.canRequestFocus = _canRequestFocus; } @override void didUpdateWidget(RichTextField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); } _effectiveFocusNode.canRequestFocus = _canRequestFocus; if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) { if (_effectiveController.selection.isCollapsed) { _showSelectionHandles = !widget.readOnly; } } if (widget.statesController == oldWidget.statesController) { _statesController ..update(WidgetState.disabled, !_isEnabled) ..update(WidgetState.hovered, _isHovering) ..update(WidgetState.focused, _effectiveFocusNode.hasFocus) ..update(WidgetState.error, _hasError); } else { oldWidget.statesController?.removeListener(_handleStatesControllerChange); if (widget.statesController != null) { _internalStatesController?.dispose(); _internalStatesController = null; } _initStatesController(); } } @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) {} @override String? get restorationId => widget.restorationId; @override void dispose() { _effectiveFocusNode.removeListener(_handleFocusChanged); _focusNode?.dispose(); _statesController.removeListener(_handleStatesControllerChange); _internalStatesController?.dispose(); super.dispose(); } EditableTextState? get _editableText => editableTextKey.currentState; void _requestKeyboard() { _editableText?.requestKeyboard(); } bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { // When the text field is activated by something that doesn't trigger the // selection toolbar, we shouldn't show the handles either. if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar || !_selectionGestureDetectorBuilder.shouldShowSelectionHandles) { return false; } if (cause == SelectionChangedCause.keyboard) { return false; } if (widget.readOnly && _effectiveController.selection.isCollapsed) { return false; } if (!_isEnabled) { return false; } if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.stylusHandwriting) { return true; } if (_effectiveController.text.isNotEmpty) { return true; } return false; } void _handleFocusChanged() { setState(() { // Rebuild the widget on focus change to show/hide the text selection // highlight. }); _statesController.update(WidgetState.focused, _effectiveFocusNode.hasFocus); } void _handleSelectionChanged( TextSelection selection, SelectionChangedCause? cause, ) { final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); if (willShowSelectionHandles != _showSelectionHandles) { setState(() { _showSelectionHandles = willShowSelectionHandles; }); } switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.fuchsia: case TargetPlatform.android: if (cause == SelectionChangedCause.longPress) { _editableText?.bringIntoView(selection.extent); } } switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.fuchsia: case TargetPlatform.android: break; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: if (cause == SelectionChangedCause.drag) { _editableText?.hideToolbar(); } } } /// Toggle the toolbar when a selection handle is tapped. void _handleSelectionHandleTapped() { if (_effectiveController.selection.isCollapsed) { _editableText!.toggleToolbar(); } } void _handleHover(bool hovering) { if (hovering != _isHovering) { setState(() { _isHovering = hovering; }); _statesController.update(WidgetState.hovered, _isHovering); } } // Material states controller. WidgetStatesController? _internalStatesController; void _handleStatesControllerChange() { // Force a rebuild to resolve WidgetStateProperty properties. setState(() {}); } WidgetStatesController get _statesController => widget.statesController ?? _internalStatesController!; void _initStatesController() { if (widget.statesController == null) { _internalStatesController = WidgetStatesController(); } _statesController ..update(WidgetState.disabled, !_isEnabled) ..update(WidgetState.hovered, _isHovering) ..update(WidgetState.focused, _effectiveFocusNode.hasFocus) ..update(WidgetState.error, _hasError) ..addListener(_handleStatesControllerChange); } // AutofillClient implementation start. @override String get autofillId => _editableText!.autofillId; @override void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue); @override TextInputConfiguration get textInputConfiguration { final List? autofillHints = widget.autofillHints?.toList( growable: false, ); final AutofillConfiguration autofillConfiguration = autofillHints != null ? AutofillConfiguration( uniqueIdentifier: autofillId, autofillHints: autofillHints, currentEditingValue: _effectiveController.value, hintText: (widget.decoration ?? const InputDecoration()).hintText, ) : AutofillConfiguration.disabled; return _editableText!.textInputConfiguration.copyWith( autofillConfiguration: autofillConfiguration, ); } // AutofillClient implementation end. TextStyle _getInputStyleForState(TextStyle style) { final ThemeData theme = Theme.of(context); final TextStyle stateStyle = WidgetStateProperty.resolveAs( theme.useMaterial3 ? _m3StateInputStyle(context)! : _m2StateInputStyle(context)!, _statesController.value, ); final TextStyle providedStyle = WidgetStateProperty.resolveAs( style, _statesController.value, ); return providedStyle.merge(stateStyle); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasDirectionality(context)); assert( !(widget.style != null && !widget.style!.inherit && (widget.style!.fontSize == null || widget.style!.textBaseline == null)), 'inherit false style must supply fontSize and textBaseline', ); final ThemeData theme = Theme.of(context); final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of( context, ); final TextStyle? providedStyle = WidgetStateProperty.resolveAs( widget.style, _statesController.value, ); final TextStyle style = _getInputStyleForState( theme.useMaterial3 ? _m3InputStyle(context) : theme.textTheme.titleMedium!, ).merge(providedStyle); final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness; final RichTextEditingController controller = _effectiveController; final FocusNode focusNode = _effectiveFocusNode; final formatters = [ ...?widget.inputFormatters, if (widget.maxLength != null) LengthLimitingTextInputFormatter( widget.maxLength, maxLengthEnforcement: _effectiveMaxLengthEnforcement, ), ]; // Set configuration as disabled if not otherwise specified. If specified, // ensure that configuration uses the correct style for misspelled words for // the current platform, unless a custom style is specified. final SpellCheckConfiguration spellCheckConfiguration; switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: spellCheckConfiguration = CupertinoRichTextField.inferIOSSpellCheckConfiguration( widget.spellCheckConfiguration, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: spellCheckConfiguration = RichTextField.inferAndroidSpellCheckConfiguration( widget.spellCheckConfiguration, ); } TextSelectionControls? textSelectionControls = widget.selectionControls; final bool paintCursorAboveText; bool? cursorOpacityAnimates = widget.cursorOpacityAnimates; Offset? cursorOffset; final Color cursorColor; final Color selectionColor; Color? autocorrectionTextRectColor; Radius? cursorRadius = widget.cursorRadius; VoidCallback? handleDidGainAccessibilityFocus; VoidCallback? handleDidLoseAccessibilityFocus; switch (theme.platform) { case TargetPlatform.iOS: final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); forcePressEnabled = true; textSelectionControls ??= cupertinoTextSelectionHandleControls; paintCursorAboveText = true; cursorOpacityAnimates ??= true; cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0, ); autocorrectionTextRectColor = selectionColor; case TargetPlatform.macOS: final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); forcePressEnabled = false; textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls; paintCursorAboveText = true; cursorOpacityAnimates ??= false; cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0, ); handleDidGainAccessibilityFocus = () { // Automatically activate the TextField when it receives accessibility focus. if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { _effectiveFocusNode.requestFocus(); } }; handleDidLoseAccessibilityFocus = () { _effectiveFocusNode.unfocus(); }; case TargetPlatform.android: case TargetPlatform.fuchsia: forcePressEnabled = false; textSelectionControls ??= materialTextSelectionHandleControls; paintCursorAboveText = false; cursorOpacityAnimates ??= false; cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withValues(alpha: 0.40); case TargetPlatform.linux: forcePressEnabled = false; textSelectionControls ??= desktopTextSelectionHandleControls; paintCursorAboveText = false; cursorOpacityAnimates ??= false; cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withValues(alpha: 0.40); handleDidGainAccessibilityFocus = () { // Automatically activate the TextField when it receives accessibility focus. if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { _effectiveFocusNode.requestFocus(); } }; handleDidLoseAccessibilityFocus = () { _effectiveFocusNode.unfocus(); }; case TargetPlatform.windows: forcePressEnabled = false; textSelectionControls ??= desktopTextSelectionHandleControls; paintCursorAboveText = false; cursorOpacityAnimates ??= false; cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withValues(alpha: 0.40); handleDidGainAccessibilityFocus = () { // Automatically activate the TextField when it receives accessibility focus. if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { _effectiveFocusNode.requestFocus(); } }; handleDidLoseAccessibilityFocus = () { _effectiveFocusNode.unfocus(); }; } Widget child = RepaintBoundary( child: UnmanagedRestorationScope( bucket: bucket, child: EditableText( key: editableTextKey, readOnly: widget.readOnly || !_isEnabled, toolbarOptions: widget.toolbarOptions, showCursor: widget.showCursor, showSelectionHandles: _showSelectionHandles, controller: controller, focusNode: focusNode, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, textCapitalization: widget.textCapitalization, style: style, strutStyle: widget.strutStyle, textAlign: widget.textAlign, textDirection: widget.textDirection, autofocus: widget.autofocus, obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, autocorrect: widget.autocorrect, smartDashesType: widget.smartDashesType, smartQuotesType: widget.smartQuotesType, enableSuggestions: widget.enableSuggestions, maxLines: widget.maxLines, minLines: widget.minLines, expands: widget.expands, // Only show the selection highlight when the text field is focused. selectionColor: focusNode.hasFocus ? selectionColor : null, selectionControls: widget.selectionEnabled ? textSelectionControls : null, onChanged: widget.onChanged, onSelectionChanged: _handleSelectionChanged, onEditingComplete: widget.onEditingComplete, onSubmitted: widget.onSubmitted, onAppPrivateCommand: widget.onAppPrivateCommand, groupId: widget.groupId, onSelectionHandleTapped: _handleSelectionHandleTapped, onTapOutside: widget.onTapOutside, onTapUpOutside: widget.onTapUpOutside, inputFormatters: formatters, rendererIgnoresPointer: true, mouseCursor: MouseCursor.defer, // TextField will handle the cursor cursorWidth: widget.cursorWidth, cursorHeight: widget.cursorHeight, cursorRadius: cursorRadius, cursorColor: cursorColor, selectionHeightStyle: widget.selectionHeightStyle, selectionWidthStyle: widget.selectionWidthStyle, cursorOpacityAnimates: cursorOpacityAnimates, cursorOffset: cursorOffset, paintCursorAboveText: paintCursorAboveText, backgroundCursorColor: CupertinoColors.inactiveGray, scrollPadding: widget.scrollPadding, keyboardAppearance: keyboardAppearance, enableInteractiveSelection: widget.enableInteractiveSelection, selectAllOnFocus: widget.selectAllOnFocus, dragStartBehavior: widget.dragStartBehavior, scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, autofillHints: widget.autofillHints, autofillClient: this, autocorrectionTextRectColor: autocorrectionTextRectColor, clipBehavior: widget.clipBehavior, restorationId: 'editable', scribbleEnabled: widget.scribbleEnabled, stylusHandwritingEnabled: widget.stylusHandwritingEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, contentInsertionConfiguration: widget.contentInsertionConfiguration, contextMenuBuilder: widget.contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, hintLocales: widget.hintLocales, ), ), ); if (widget.decoration != null) { child = AnimatedBuilder( animation: Listenable.merge([focusNode, controller]), builder: (BuildContext context, Widget? child) { return InputDecorator( decoration: _getEffectiveDecoration(), baseStyle: widget.style, textAlign: widget.textAlign, textAlignVertical: widget.textAlignVertical, isHovering: _isHovering, isFocused: focusNode.hasFocus, isEmpty: controller.value.text.isEmpty, expands: widget.expands, child: child, ); }, child: child, ); } final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs( widget.mouseCursor ?? WidgetStateMouseCursor.textable, _statesController.value, ); final int? semanticsMaxValueLength; if (_effectiveMaxLengthEnforcement != MaxLengthEnforcement.none && widget.maxLength != null && widget.maxLength! > 0) { semanticsMaxValueLength = widget.maxLength; } else { semanticsMaxValueLength = null; } return MouseRegion( cursor: effectiveMouseCursor, onEnter: (PointerEnterEvent event) => _handleHover(true), onExit: (PointerExitEvent event) => _handleHover(false), child: TextFieldTapRegion( child: IgnorePointer( ignoring: widget.ignorePointers ?? !_isEnabled, child: AnimatedBuilder( animation: controller, // changes the _currentLength builder: (BuildContext context, Widget? child) { return Semantics( enabled: _isEnabled, maxValueLength: semanticsMaxValueLength, currentValueLength: _currentLength, onTap: widget.readOnly ? null : () { if (!_effectiveController.selection.isValid) { _effectiveController.selection = TextSelection.collapsed( offset: _effectiveController.text.length, ); } _requestKeyboard(); }, onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus, onFocus: _isEnabled ? () { assert( _effectiveFocusNode.canRequestFocus, 'Received SemanticsAction.focus from the engine. However, the FocusNode ' 'of this text field cannot gain focus. This likely indicates a bug. ' 'If this text field cannot be focused (e.g. because it is not ' 'enabled), then its corresponding semantics node must be configured ' 'such that the assistive technology cannot request focus on it.', ); if (_effectiveFocusNode.canRequestFocus && !_effectiveFocusNode.hasFocus) { _effectiveFocusNode.requestFocus(); } else if (!widget.readOnly) { // If the platform requested focus, that means that previously the // platform believed that the text field did not have focus (even // though Flutter's widget system believed otherwise). This likely // means that the on-screen keyboard is hidden, or more generally, // there is no current editing session in this field. To correct // that, keyboard must be requested. // // A concrete scenario where this can happen is when the user // dismisses the keyboard on the web. The editing session is // closed by the engine, but the text field widget stays focused // in the framework. _requestKeyboard(); } } : null, child: child, ); }, child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: child, ), ), ), ), ); } void scheduleShowCaretOnScreen({required bool withAnimation}) { _editableText?.scheduleShowCaretOnScreen(withAnimation: withAnimation); } } TextStyle? _m2StateInputStyle(BuildContext context) => WidgetStateTextStyle.resolveWith((Set states) { final ThemeData theme = Theme.of(context); if (states.contains(WidgetState.disabled)) { return TextStyle(color: theme.disabledColor); } return TextStyle(color: theme.textTheme.titleMedium?.color); }); TextStyle _m2CounterErrorStyle(BuildContext context) => Theme.of( context, ).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error); // BEGIN GENERATED TOKEN PROPERTIES - TextField // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // dart format off TextStyle? _m3StateInputStyle(BuildContext context) => WidgetStateTextStyle.resolveWith((Set states) { if (states.contains(WidgetState.disabled)) { return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withValues(alpha:0.38)); } return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color); }); TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!; TextStyle _m3CounterErrorStyle(BuildContext context) => Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error); // dart format on // END GENERATED TOKEN PROPERTIES - TextField ================================================ FILE: lib/common/widgets/flutter/text_field/text_selection.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /// @docImport 'package:flutter/cupertino.dart'; /// @docImport 'package:flutter/material.dart'; library; import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide EditableText, EditableTextState; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; /// Delegate interface for the [TextSelectionGestureDetectorBuilder]. /// /// The interface is usually implemented by the [State] of text field /// implementations wrapping [EditableText], so that they can use a /// [TextSelectionGestureDetectorBuilder] to build a /// [TextSelectionGestureDetector] for their [EditableText]. The delegate /// provides the builder with information about the current state of the text /// field. Based on that information, the builder adds the correct gesture /// handlers to the gesture detector. /// /// See also: /// /// * [TextField], which implements this delegate for the Material text field. /// * [CupertinoTextField], which implements this delegate for the Cupertino /// text field. abstract class TextSelectionGestureDetectorBuilderDelegate { /// [GlobalKey] to the [EditableText] for which the /// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector]. GlobalKey get editableTextKey; /// Whether the text field should respond to force presses. bool get forcePressEnabled; /// Whether the user may select text in the text field. bool get selectionEnabled; } /// Builds a [TextSelectionGestureDetector] to wrap an [EditableText]. /// /// The class implements sensible defaults for many user interactions /// with an [EditableText] (see the documentation of the various gesture handler /// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of /// [TextSelectionGestureDetectorBuilder] can change the behavior performed in /// responds to these gesture events by overriding the corresponding handler /// methods of this class. /// /// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is /// obtained by calling [buildGestureDetector]. /// /// A [TextSelectionGestureDetectorBuilder] must be provided a /// [TextSelectionGestureDetectorBuilderDelegate], from which information about /// the [EditableText] may be obtained. Typically, the [State] of the widget /// that builds the [EditableText] implements this interface, and then passes /// itself as the [delegate]. /// /// See also: /// /// * [TextField], which uses a subclass to implement the Material-specific /// gesture logic of an [EditableText]. /// * [CupertinoTextField], which uses a subclass to implement the /// Cupertino-specific gesture logic of an [EditableText]. class TextSelectionGestureDetectorBuilder { /// Creates a [TextSelectionGestureDetectorBuilder]. TextSelectionGestureDetectorBuilder({ required this.delegate, required this.controller, }); /// The delegate for this [TextSelectionGestureDetectorBuilder]. /// /// The delegate provides the builder with information about what actions can /// currently be performed on the text field. Based on this, the builder adds /// the correct gesture handlers to the gesture detector. /// /// Typically implemented by a [State] of a widget that builds an /// [EditableText]. @protected final TextSelectionGestureDetectorBuilderDelegate delegate; final RichTextEditingController controller; // Shows the magnifier on supported platforms at the given offset, currently // only Android and iOS. void _showMagnifierIfSupportedByPlatform(Offset positionToShow) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: editableText.showMagnifier(positionToShow); case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: } } // Hides the magnifier on supported platforms, currently only Android and iOS. void _hideMagnifierIfSupportedByPlatform() { if (!_isEditableTextMounted) { return; } switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: editableText.hideMagnifier(); case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: } } /// Returns true if lastSecondaryTapDownPosition was on selection. bool get _lastSecondaryTapWasOnSelection { assert(renderEditable.lastSecondaryTapDownPosition != null); if (renderEditable.selection == null) { return false; } final TextPosition textPosition = renderEditable.getPositionForPoint( renderEditable.lastSecondaryTapDownPosition!, ); return renderEditable.selection!.start <= textPosition.offset && renderEditable.selection!.end >= textPosition.offset; } bool _positionWasOnSelectionExclusive(TextPosition textPosition) { final TextSelection? selection = renderEditable.selection; if (selection == null) { return false; } return selection.start < textPosition.offset && selection.end > textPosition.offset; } bool _positionWasOnSelectionInclusive(TextPosition textPosition) { final TextSelection? selection = renderEditable.selection; if (selection == null) { return false; } return selection.start <= textPosition.offset && selection.end >= textPosition.offset; } // Expand the selection to the given global position. // // Either base or extent will be moved to the last tapped position, whichever // is closest. The selection will never shrink or pivot, only grow. // // If fromSelection is given, will expand from that selection instead of the // current selection in renderEditable. // // See also: // // * [_extendSelection], which is similar but pivots the selection around // the base. void _expandSelection( Offset offset, SelectionChangedCause cause, [ TextSelection? fromSelection, ]) { assert(renderEditable.selection?.baseOffset != null); final TextPosition tappedPosition = renderEditable.getPositionForPoint( offset, ); final TextSelection selection = fromSelection ?? renderEditable.selection!; final bool baseIsCloser = (tappedPosition.offset - selection.baseOffset).abs() < (tappedPosition.offset - selection.extentOffset).abs(); final TextSelection nextSelection = selection.copyWith( baseOffset: baseIsCloser ? selection.extentOffset : selection.baseOffset, extentOffset: tappedPosition.offset, ); editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith(selection: nextSelection), cause, ); } // Extend the selection to the given global position. // // Holds the base in place and moves the extent. // // See also: // // * [_expandSelection], which is similar but always increases the size of // the selection. void _extendSelection(Offset offset, SelectionChangedCause cause) { assert(renderEditable.selection?.baseOffset != null); final TextPosition tappedPosition = renderEditable.getPositionForPoint( offset, ); final TextSelection selection = renderEditable.selection!; // bggRGjQaUbCoE on select final TextSelection nextSelection = selection.copyWith( extentOffset: controller.tapOffsetSimple(tappedPosition.offset), ); editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith(selection: nextSelection), cause, ); } /// Whether to show the selection toolbar. /// /// It is based on the signal source when [onTapDown], [onSecondaryTapDown], /// [onDragSelectionStart], or [onForcePressStart] is called. This getter /// will return true if the current [onTapDown], or [onDragSelectionStart] event /// is triggered by a touch or a stylus. It will always return true for the /// current [onSecondaryTapDown] or [onForcePressStart] event. bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar; bool _shouldShowSelectionToolbar = true; /// Whether to show the selection handles. /// /// It is based on the signal source when [onTapDown], [onSecondaryTapDown], /// [onDragSelectionStart], is called. This getter will return true if the /// current [onTapDown], [onSecondaryTapDown], or [onDragSelectionStart] event /// is triggered by a touch or a stylus. bool get shouldShowSelectionHandles => _shouldShowSelectionHandles; bool _shouldShowSelectionHandles = true; /// The [State] of the [EditableText] for which the builder will provide a /// [TextSelectionGestureDetector]. @protected EditableTextState get editableText => delegate.editableTextKey.currentState!; /// The [RenderObject] of the [EditableText] for which the builder will /// provide a [TextSelectionGestureDetector]. @protected RenderEditable get renderEditable => editableText.renderEditable; /// Returns `true` if a widget with the global key [delegate.editableTextKey] /// is in the tree and the widget is mounted. /// /// Otherwise returns `false`. bool get _isEditableTextMounted => delegate.editableTextKey.currentContext?.mounted ?? false; /// Whether the Shift key was pressed when the most recent [PointerDownEvent] /// was tracked by the [BaseTapAndDragGestureRecognizer]. bool _isShiftPressed = false; /// The viewport offset pixels of any [Scrollable] containing the /// [RenderEditable] at the last drag start. double _dragStartScrollOffset = 0.0; /// The viewport offset pixels of the [RenderEditable] at the last drag start. double _dragStartViewportOffset = 0.0; double get _scrollPosition { final ScrollableState? scrollableState = delegate.editableTextKey.currentContext == null ? null : Scrollable.maybeOf(delegate.editableTextKey.currentContext!); return scrollableState == null ? 0.0 : scrollableState.position.pixels; } AxisDirection? get _scrollDirection { final ScrollableState? scrollableState = delegate.editableTextKey.currentContext == null ? null : Scrollable.maybeOf(delegate.editableTextKey.currentContext!); return scrollableState?.axisDirection; } // For a shift + tap + drag gesture, the TextSelection at the point of the // tap. Mac uses this value to reset to the original selection when an // inversion of the base and offset happens. TextSelection? _dragStartSelection; // For iOS long press behavior when the field is not focused. iOS uses this value // to determine if a long press began on a field that was not focused. // // If the field was not focused when the long press began, a long press will select // the word and a long press move will select word-by-word. If the field was // focused, the cursor moves to the long press position. bool _longPressStartedWithoutFocus = false; /// Handler for [TextSelectionGestureDetector.onTapTrackStart]. /// /// See also: /// /// * [TextSelectionGestureDetector.onTapTrackStart], which triggers this /// callback. @protected void onTapTrackStart() { _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed.intersection( { LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight, }, ).isNotEmpty; } /// Handler for [TextSelectionGestureDetector.onTapTrackReset]. /// /// See also: /// /// * [TextSelectionGestureDetector.onTapTrackReset], which triggers this /// callback. @protected void onTapTrackReset() { _isShiftPressed = false; } /// Handler for [TextSelectionGestureDetector.onTapDown]. /// /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus. /// /// See also: /// /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback. @protected void onTapDown(TapDragDownDetails details) { if (!delegate.selectionEnabled) { return; } // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state // in renderEditable. The gesture callbacks can use the details objects directly // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap] // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in // renderEditable. When this migration is complete we should remove this hack. // See https://github.com/flutter/flutter/issues/115130. renderEditable.handleTapDown( TapDownDetails(globalPosition: details.globalPosition), ); // The selection overlay should only be shown when the user is interacting // through a touch screen (via either a finger or a stylus). A mouse shouldn't // trigger the selection overlay. // For backwards-compatibility, we treat a null kind the same as touch. final PointerDeviceKind? kind = details.kind; // TODO(justinmc): Should a desktop platform show its selection toolbar when // receiving a tap event? Say a Windows device with a touchscreen. // https://github.com/flutter/flutter/issues/106586 _shouldShowSelectionToolbar = kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; _shouldShowSelectionHandles = _shouldShowSelectionToolbar; // It is impossible to extend the selection when the shift key is pressed, if the // renderEditable.selection is invalid. final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { case TargetPlatform.android: if (editableText.widget.stylusHandwritingEnabled) { final bool stylusEnabled = switch (kind) { PointerDeviceKind.stylus || PointerDeviceKind.invertedStylus => editableText.widget.stylusHandwritingEnabled, _ => false, }; if (stylusEnabled) { Scribe.isFeatureAvailable().then((bool isAvailable) { if (isAvailable) { renderEditable.selectPosition( cause: SelectionChangedCause.stylusHandwriting, ); Scribe.startStylusHandwriting(); } }); } } case TargetPlatform.fuchsia: case TargetPlatform.iOS: // On mobile platforms the selection is set on tap up. break; case TargetPlatform.macOS: editableText.hideToolbar(); // On macOS, a shift-tapped unfocused field expands from 0, not from the // previous selection. if (isShiftPressedValid) { final TextSelection? fromSelection = renderEditable.hasFocus ? null : const TextSelection.collapsed(offset: 0); _expandSelection( details.globalPosition, SelectionChangedCause.tap, fromSelection, ); return; } // On macOS, a tap/click places the selection in a precise position. // This differs from iOS/iPadOS, where if the gesture is done by a touch // then the selection moves to the closest word edge, instead of a // precise position. renderEditable.selectPosition(cause: SelectionChangedCause.tap); case TargetPlatform.linux: case TargetPlatform.windows: editableText.hideToolbar(); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); } } /// Handler for [TextSelectionGestureDetector.onForcePressStart]. /// /// By default, it selects the word at the position of the force press, /// if selection is enabled. /// /// This callback is only applicable when force press is enabled. /// /// See also: /// /// * [TextSelectionGestureDetector.onForcePressStart], which triggers this /// callback. @protected void onForcePressStart(ForcePressDetails details) { assert(delegate.forcePressEnabled); _shouldShowSelectionToolbar = true; if (!delegate.selectionEnabled) { return; } renderEditable.selectWordsInRange( from: details.globalPosition, cause: SelectionChangedCause.forcePress, ); editableText.showToolbar(); } /// Handler for [TextSelectionGestureDetector.onForcePressEnd]. /// /// By default, it selects words in the range specified in [details] and shows /// toolbar if it is necessary. /// /// This callback is only applicable when force press is enabled. /// /// See also: /// /// * [TextSelectionGestureDetector.onForcePressEnd], which triggers this /// callback. @protected void onForcePressEnd(ForcePressDetails details) { assert(delegate.forcePressEnabled); renderEditable.selectWordsInRange( from: details.globalPosition, cause: SelectionChangedCause.forcePress, ); if (shouldShowSelectionToolbar) { editableText.showToolbar(); } } /// Whether the provided [onUserTap] callback should be dispatched on every /// tap or only non-consecutive taps. /// /// Defaults to false. @protected bool get onUserTapAlwaysCalled => false; /// Handler for [TextSelectionGestureDetector.onUserTap]. /// /// By default, it serves as placeholder to enable subclass override. /// /// See also: /// /// * [TextSelectionGestureDetector.onUserTap], which triggers this /// callback. /// * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls /// whether this callback is called only on the first tap in a series /// of taps. @protected void onUserTap() { /* Subclass should override this method if needed. */ } /// Handler for [TextSelectionGestureDetector.onSingleTapUp]. /// /// By default, it selects word edge if selection is enabled. /// /// See also: /// /// * [TextSelectionGestureDetector.onSingleTapUp], which triggers /// this callback. @protected void onSingleTapUp(TapDragUpDetails details) { if (!delegate.selectionEnabled) { editableText.requestKeyboard(); return; } // It is impossible to extend the selection when the shift key is pressed, if the // renderEditable.selection is invalid. final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: break; // On desktop platforms the selection is set on tap down. case TargetPlatform.android: editableText.hideToolbar(false); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); editableText.showSpellCheckSuggestionsToolbar(); case TargetPlatform.fuchsia: editableText.hideToolbar(false); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); case TargetPlatform.iOS: if (isShiftPressedValid) { // On iOS, a shift-tapped unfocused field expands from 0, not from // the previous selection. final TextSelection? fromSelection = renderEditable.hasFocus ? null : const TextSelection.collapsed(offset: 0); _expandSelection( details.globalPosition, SelectionChangedCause.tap, fromSelection, ); return; } switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: // TODO(camsim99): Determine spell check toolbar behavior in these cases: // https://github.com/flutter/flutter/issues/119573. // Precise devices should place the cursor at a precise position if the // word at the text position is not misspelled. renderEditable.selectPosition(cause: SelectionChangedCause.tap); editableText.hideToolbar(); case PointerDeviceKind.touch: case PointerDeviceKind.unknown: // If the word that was tapped is misspelled, select the word and show the spell check suggestions // toolbar once. If additional taps are made on a misspelled word, toggle the toolbar. If the word // is not misspelled, default to the following behavior: // // Toggle the toolbar when the tap is exclusively within the bounds of a non-collapsed `previousSelection`, // and the editable is focused. // // Toggle the toolbar if the `previousSelection` is collapsed, the tap is on the selection, the // TextAffinity remains the same, the editable field is not read only, and the editable is focused. // The TextAffinity is important when the cursor is on the boundary of a line wrap, if the affinity // is different (i.e. it is downstream), the selection should move to the following line and not toggle // the toolbar. // // Selects the word edge closest to the tap when the editable is not focused, or if the tap was neither exclusively // or inclusively on `previousSelection`. If the selection remains the same after selecting the word edge, then we // toggle the toolbar, if the editable field is not read only. If the selection changes then we hide the toolbar. final TextSelection previousSelection = renderEditable.selection ?? editableText.textEditingValue.selection; final TextPosition textPosition = renderEditable .getPositionForPoint( details.globalPosition, ); final bool isAffinityTheSame = textPosition.affinity == previousSelection.affinity; final bool wordAtCursorIndexIsMisspelled = editableText.findSuggestionSpanAtCursorIndex( textPosition.offset, ) != null; if (wordAtCursorIndexIsMisspelled) { renderEditable.selectWord(cause: SelectionChangedCause.tap); if (previousSelection != editableText.textEditingValue.selection) { editableText.showSpellCheckSuggestionsToolbar(); } else { editableText.toggleToolbar(false); } } else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame && !renderEditable.readOnly)) && renderEditable.hasFocus) { editableText.toggleToolbar(false); } else { renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); if (previousSelection == editableText.textEditingValue.selection && renderEditable.hasFocus && !renderEditable.readOnly) { editableText.toggleToolbar(false); } else { editableText.hideToolbar(false); } } } } editableText.requestKeyboard(); } /// Handler for [TextSelectionGestureDetector.onSingleTapCancel]. /// /// By default, it serves as placeholder to enable subclass override. /// /// See also: /// /// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers /// this callback. @protected void onSingleTapCancel() { /* Subclass should override this method if needed. */ } /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart]. /// /// By default, it selects text position specified in [details] if selection /// is enabled. /// /// See also: /// /// * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers /// this callback. @protected void onSingleLongTapStart(LongPressStartDetails details) { if (!delegate.selectionEnabled) { return; } switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: if (!renderEditable.hasFocus) { _longPressStartedWithoutFocus = true; renderEditable.selectWord(cause: SelectionChangedCause.longPress); } else if (renderEditable.readOnly) { renderEditable.selectWord(cause: SelectionChangedCause.longPress); if (editableText.context.mounted) { Feedback.forLongPress(editableText.context); } } else { renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.longPress, ); // Show the floating cursor. final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( state: FloatingCursorDragState.Start, startLocation: ( renderEditable.globalToLocal(details.globalPosition), TextPosition( offset: editableText.textEditingValue.selection.baseOffset, affinity: editableText.textEditingValue.selection.affinity, ), ), offset: Offset.zero, ); editableText.updateFloatingCursor(cursorPoint); } case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: renderEditable.selectWord(cause: SelectionChangedCause.longPress); if (editableText.context.mounted) { Feedback.forLongPress(editableText.context); } } _showMagnifierIfSupportedByPlatform(details.globalPosition); _dragStartViewportOffset = renderEditable.offset.pixels; _dragStartScrollOffset = _scrollPosition; } /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate]. /// /// By default, it updates the selection location specified in [details] if /// selection is enabled. /// /// See also: /// /// * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which /// triggers this callback. @protected void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { if (!delegate.selectionEnabled) { return; } // Adjust the drag start offset for possible viewport offset changes. final Offset editableOffset = renderEditable.maxLines == 1 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset); final Offset scrollableOffset = switch (axisDirectionToAxis( _scrollDirection ?? AxisDirection.left, )) { Axis.horizontal => Offset(_scrollPosition - _dragStartScrollOffset, 0.0), Axis.vertical => Offset(0.0, _scrollPosition - _dragStartScrollOffset), }; switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: if (_longPressStartedWithoutFocus || renderEditable.readOnly) { renderEditable.selectWordsInRange( from: details.globalPosition - details.offsetFromOrigin - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.longPress, ); } else { renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.longPress, ); // Update the floating cursor. final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( state: FloatingCursorDragState.Update, offset: details.offsetFromOrigin, ); editableText.updateFloatingCursor(cursorPoint); } case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: renderEditable.selectWordsInRange( from: details.globalPosition - details.offsetFromOrigin - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.longPress, ); } _showMagnifierIfSupportedByPlatform(details.globalPosition); } /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd]. /// /// By default, it shows toolbar if necessary. /// /// See also: /// /// * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this /// callback. @protected void onSingleLongTapEnd(LongPressEndDetails details) { _onSingleLongTapEndOrCancel(); if (shouldShowSelectionToolbar) { editableText.showToolbar(); } } /// Handler for [TextSelectionGestureDetector.onSingleLongTapCancel]. /// /// By default, it hides the magnifier and the floating cursor if necessary. /// /// See also: /// /// * [TextSelectionGestureDetector.onSingleLongTapCancel], which triggers /// this callback. @protected void onSingleLongTapCancel() { _onSingleLongTapEndOrCancel(); } /// Handler for [TextSelectionGestureDetector.onSecondaryTap]. /// /// By default, selects the word if possible and shows the toolbar. @protected void onSecondaryTap() { if (!delegate.selectionEnabled) { return; } switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: if (!_lastSecondaryTapWasOnSelection || !renderEditable.hasFocus) { renderEditable.selectWord(cause: SelectionChangedCause.tap); } if (shouldShowSelectionToolbar) { editableText.hideToolbar(); editableText.showToolbar(); } case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: if (!renderEditable.hasFocus) { renderEditable.selectPosition(cause: SelectionChangedCause.tap); } editableText.toggleToolbar(); } } /// Handler for [TextSelectionGestureDetector.onSecondaryTapDown]. /// /// See also: /// /// * [TextSelectionGestureDetector.onSecondaryTapDown], which triggers this /// callback. /// * [onSecondaryTap], which is typically called after this. @protected void onSecondaryTapDown(TapDownDetails details) { // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state // in renderEditable. The gesture callbacks can use the details objects directly // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap] // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in // renderEditable. When this migration is complete we should remove this hack. // See https://github.com/flutter/flutter/issues/115130. renderEditable.handleSecondaryTapDown( TapDownDetails(globalPosition: details.globalPosition), ); _shouldShowSelectionToolbar = true; _shouldShowSelectionHandles = details.kind == null || details.kind == PointerDeviceKind.touch || details.kind == PointerDeviceKind.stylus; } /// Handler for [TextSelectionGestureDetector.onDoubleTapDown]. /// /// By default, it selects a word through [RenderEditable.selectWord] if /// selectionEnabled and shows toolbar if necessary. /// /// See also: /// /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this /// callback. @protected void onDoubleTapDown(TapDragDownDetails details) { if (delegate.selectionEnabled) { renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); if (shouldShowSelectionToolbar) { editableText.showToolbar(); } } } void _onSingleLongTapEndOrCancel() { _hideMagnifierIfSupportedByPlatform(); _longPressStartedWithoutFocus = false; _dragStartViewportOffset = 0.0; _dragStartScrollOffset = 0.0; if (_isEditableTextMounted && defaultTargetPlatform == TargetPlatform.iOS && delegate.selectionEnabled && editableText.textEditingValue.selection.isCollapsed) { // Update the floating cursor. final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( state: FloatingCursorDragState.End, ); editableText.updateFloatingCursor(cursorPoint); } } // Selects the set of paragraphs in a document that intersect a given range of // global positions. void _selectParagraphsInRange({ required Offset from, Offset? to, SelectionChangedCause? cause, }) { final TextBoundary paragraphBoundary = ParagraphBoundary( editableText.textEditingValue.text, ); _selectTextBoundariesInRange( boundary: paragraphBoundary, from: from, to: to, cause: cause, ); } // Selects the set of lines in a document that intersect a given range of // global positions. void _selectLinesInRange({ required Offset from, Offset? to, SelectionChangedCause? cause, }) { final TextBoundary lineBoundary = LineBoundary(renderEditable); _selectTextBoundariesInRange( boundary: lineBoundary, from: from, to: to, cause: cause, ); } // Returns the location of a text boundary at `extent`. When `extent` is at // the end of the text, returns the previous text boundary's location. TextRange _moveToTextBoundary( TextPosition extent, TextBoundary textBoundary, ) { assert(extent.offset >= 0); // Use extent.offset - 1 when `extent` is at the end of the text to retrieve // the previous text boundary's location. final int start = textBoundary.getLeadingTextBoundaryAt( extent.offset == editableText.textEditingValue.text.length ? extent.offset - 1 : extent.offset, ) ?? 0; final int end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? editableText.textEditingValue.text.length; return TextRange(start: start, end: end); } // Selects the set of text boundaries in a document that intersect a given // range of global positions. // // The set of text boundaries selected are not strictly bounded by the range // of global positions. // // The first and last endpoints of the selection will always be at the // beginning and end of a text boundary respectively. void _selectTextBoundariesInRange({ required TextBoundary boundary, required Offset from, Offset? to, SelectionChangedCause? cause, }) { final TextPosition fromPosition = renderEditable.getPositionForPoint(from); final TextRange fromRange = _moveToTextBoundary(fromPosition, boundary); final TextPosition toPosition = to == null ? fromPosition : renderEditable.getPositionForPoint(to); final TextRange toRange = toPosition == fromPosition ? fromRange : _moveToTextBoundary(toPosition, boundary); final bool isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end; final TextSelection newSelection = isFromBoundaryBeforeToBoundary ? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end) : TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start); editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith(selection: newSelection), cause, ); } /// Handler for [TextSelectionGestureDetector.onTripleTapDown]. /// /// By default, it selects a paragraph if /// [TextSelectionGestureDetectorBuilderDelegate.selectionEnabled] is true /// and shows the toolbar if necessary. /// /// See also: /// /// * [TextSelectionGestureDetector.onTripleTapDown], which triggers this /// callback. @protected void onTripleTapDown(TapDragDownDetails details) { if (!delegate.selectionEnabled) { return; } if (renderEditable.maxLines == 1) { editableText.selectAll(SelectionChangedCause.tap); } else { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: case TargetPlatform.macOS: case TargetPlatform.windows: _selectParagraphsInRange( from: details.globalPosition, cause: SelectionChangedCause.tap, ); case TargetPlatform.linux: _selectLinesInRange( from: details.globalPosition, cause: SelectionChangedCause.tap, ); } } if (shouldShowSelectionToolbar) { editableText.showToolbar(); } } /// Handler for [TextSelectionGestureDetector.onDragSelectionStart]. /// /// By default, it selects a text position specified in [details]. /// /// See also: /// /// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers /// this callback. @protected void onDragSelectionStart(TapDragStartDetails details) { if (!delegate.selectionEnabled) { return; } final PointerDeviceKind? kind = details.kind; _shouldShowSelectionToolbar = kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; _shouldShowSelectionHandles = _shouldShowSelectionToolbar; _dragStartSelection = renderEditable.selection; _dragStartScrollOffset = _scrollPosition; _dragStartViewportOffset = renderEditable.offset.pixels; if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount( details.consecutiveTapCount, ) > 1) { // Do not set the selection on a consecutive tap and drag. return; } if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: _expandSelection(details.globalPosition, SelectionChangedCause.drag); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: _extendSelection(details.globalPosition, SelectionChangedCause.drag); } } else { switch (defaultTargetPlatform) { case TargetPlatform.iOS: switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, ); case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: case null: } case TargetPlatform.android: case TargetPlatform.fuchsia: switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, ); case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: // For Android, Fuchsia, and iOS platforms, a touch drag // does not initiate unless the editable has focus. if (renderEditable.hasFocus) { renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, ); _showMagnifierIfSupportedByPlatform(details.globalPosition); } case null: } case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, ); } } } /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate]. /// /// By default, it updates the selection location specified in the provided /// details objects. /// /// See also: /// /// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers /// this callback./lib/src/material/text_field.dart @protected void onDragSelectionUpdate(TapDragUpdateDetails details) { if (!delegate.selectionEnabled) { return; } if (!_isShiftPressed) { // Adjust the drag start offset for possible viewport offset changes. final Offset editableOffset = renderEditable.maxLines == 1 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) : Offset( 0.0, renderEditable.offset.pixels - _dragStartViewportOffset, ); final Offset scrollableOffset = switch (axisDirectionToAxis( _scrollDirection ?? AxisDirection.left, )) { Axis.horizontal => Offset( _scrollPosition - _dragStartScrollOffset, 0.0, ), Axis.vertical => Offset(0.0, _scrollPosition - _dragStartScrollOffset), }; final Offset dragStartGlobalPosition = details.globalPosition - details.offsetFromOrigin; // Select word by word. if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount( details.consecutiveTapCount, ) == 2) { renderEditable.selectWordsInRange( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); switch (details.kind) { case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: return _showMagnifierIfSupportedByPlatform(details.globalPosition); case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: case null: return; } } // Select paragraph-by-paragraph. if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount( details.consecutiveTapCount, ) == 3) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: return _selectParagraphsInRange( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: case null: // Triple tap to drag is not present on these platforms when using // non-precise pointer devices at the moment. break; } return; case TargetPlatform.linux: return _selectLinesInRange( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); case TargetPlatform.windows: case TargetPlatform.macOS: return _selectParagraphsInRange( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); } } switch (defaultTargetPlatform) { case TargetPlatform.iOS: // With a mouse device, a drag should select the range from the origin of the drag // to the current position of the drag. // // With a touch device, nothing should happen. switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: return renderEditable.selectPositionAt( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: case null: break; } return; case TargetPlatform.android: case TargetPlatform.fuchsia: // With a precise pointer device, such as a mouse, trackpad, or stylus, // the drag will select the text spanning the origin of the drag to the end of the drag. // With a touch device, the cursor should move with the drag. switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: return renderEditable.selectPositionAt( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); case PointerDeviceKind.touch: case PointerDeviceKind.unknown: if (renderEditable.hasFocus) { renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, ); return _showMagnifierIfSupportedByPlatform( details.globalPosition, ); } case null: break; } return; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: return renderEditable.selectPositionAt( from: dragStartGlobalPosition - editableOffset - scrollableOffset, to: details.globalPosition, cause: SelectionChangedCause.drag, ); } } if (_dragStartSelection!.isCollapsed || (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.macOS)) { return _extendSelection( details.globalPosition, SelectionChangedCause.drag, ); } // If the drag inverts the selection, Mac and iOS revert to the initial // selection. final TextSelection selection = editableText.textEditingValue.selection; final TextPosition nextExtent = renderEditable.getPositionForPoint( details.globalPosition, ); final bool isShiftTapDragSelectionForward = _dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset; final bool isInverted = isShiftTapDragSelectionForward ? nextExtent.offset < _dragStartSelection!.baseOffset : nextExtent.offset > _dragStartSelection!.baseOffset; if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) { editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith( selection: TextSelection( baseOffset: _dragStartSelection!.extentOffset, extentOffset: nextExtent.offset, ), ), SelectionChangedCause.drag, ); } else if (!isInverted && nextExtent.offset != _dragStartSelection!.baseOffset && selection.baseOffset != _dragStartSelection!.baseOffset) { editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith( selection: TextSelection( baseOffset: _dragStartSelection!.baseOffset, extentOffset: nextExtent.offset, ), ), SelectionChangedCause.drag, ); } else { _extendSelection(details.globalPosition, SelectionChangedCause.drag); } } /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd]. /// /// By default, it cleans up the state used for handling certain /// built-in behaviors. /// /// See also: /// /// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this /// callback. @protected void onDragSelectionEnd(TapDragEndDetails details) { if (_shouldShowSelectionToolbar && _TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount( details.consecutiveTapCount, ) == 2) { editableText.showToolbar(); } if (_isShiftPressed) { _dragStartSelection = null; } _hideMagnifierIfSupportedByPlatform(); } /// Returns a [TextSelectionGestureDetector] configured with the handlers /// provided by this builder. /// /// The [child] or its subtree should contain an [EditableText] whose key is /// the [GlobalKey] provided by the [delegate]'s /// [TextSelectionGestureDetectorBuilderDelegate.editableTextKey]. Widget buildGestureDetector({ Key? key, HitTestBehavior? behavior, required Widget child, }) { return TextSelectionGestureDetector( key: key, onTapTrackStart: onTapTrackStart, onTapTrackReset: onTapTrackReset, onTapDown: onTapDown, onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, onSecondaryTap: onSecondaryTap, onSecondaryTapDown: onSecondaryTapDown, onSingleTapUp: onSingleTapUp, onSingleTapCancel: onSingleTapCancel, onUserTap: onUserTap, onSingleLongTapStart: onSingleLongTapStart, onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapEnd: onSingleLongTapEnd, onSingleLongTapCancel: onSingleLongTapCancel, onDoubleTapDown: onDoubleTapDown, onTripleTapDown: onTripleTapDown, onDragSelectionStart: onDragSelectionStart, onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionEnd: onDragSelectionEnd, onUserTapAlwaysCalled: onUserTapAlwaysCalled, behavior: behavior, child: child, ); } } /// A gesture detector to respond to non-exclusive event chains for a text field. /// /// An ordinary [GestureDetector] configured to handle events like tap and /// double tap will only recognize one or the other. This widget detects both: /// the first tap and then any subsequent taps that occurs within a time limit /// after the first. /// /// See also: /// /// * [TextField], a Material text field which uses this gesture detector. /// * [CupertinoTextField], a Cupertino text field which uses this gesture /// detector. class TextSelectionGestureDetector extends StatefulWidget { /// Create a [TextSelectionGestureDetector]. /// /// Multiple callbacks can be called for one sequence of input gesture. const TextSelectionGestureDetector({ super.key, this.onTapTrackStart, this.onTapTrackReset, this.onTapDown, this.onForcePressStart, this.onForcePressEnd, this.onSecondaryTap, this.onSecondaryTapDown, this.onSingleTapUp, this.onSingleTapCancel, this.onUserTap, this.onSingleLongTapStart, this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, this.onSingleLongTapCancel, this.onDoubleTapDown, this.onTripleTapDown, this.onDragSelectionStart, this.onDragSelectionUpdate, this.onDragSelectionEnd, this.onUserTapAlwaysCalled = false, this.behavior, required this.child, }); /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart} /// Callback used to indicate that a tap tracking has started upon /// a [PointerDownEvent]. /// {@endtemplate} final VoidCallback? onTapTrackStart; /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset} /// Callback used to indicate that a tap tracking has been reset which /// happens on the next [PointerDownEvent] after the timer between two taps /// elapses, the recognizer loses the arena, the gesture is cancelled or /// the recognizer is disposed of. /// {@endtemplate} final VoidCallback? onTapTrackReset; /// Called for every tap down including every tap down that's part of a /// double click or a long press, except touches that include enough movement /// to not qualify as taps (e.g. pans and flings). final GestureTapDragDownCallback? onTapDown; /// Called when a pointer has tapped down and the force of the pointer has /// just become greater than [ForcePressGestureRecognizer.startPressure]. final GestureForcePressStartCallback? onForcePressStart; /// Called when a pointer that had previously triggered [onForcePressStart] is /// lifted off the screen. final GestureForcePressEndCallback? onForcePressEnd; /// Called for a tap event with the secondary mouse button. final GestureTapCallback? onSecondaryTap; /// Called for a tap down event with the secondary mouse button. final GestureTapDownCallback? onSecondaryTapDown; /// Called for the first tap in a series of taps, consecutive taps do not call /// this method. /// /// For example, if the detector was configured with [onTapDown] and /// [onDoubleTapDown], three quick taps would be recognized as a single tap /// down, followed by a tap up, then a double tap down, followed by a single tap down. final GestureTapDragUpCallback? onSingleTapUp; /// Called for each touch that becomes recognized as a gesture that is not a /// short tap, such as a long tap or drag. It is called at the moment when /// another gesture from the touch is recognized. final GestureCancelCallback? onSingleTapCancel; /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is /// disabled, which is the default behavior. /// /// When [onUserTapAlwaysCalled] is enabled, this is called for every tap, /// including consecutive taps. final GestureTapCallback? onUserTap; /// Called for a single long tap that's sustained for longer than /// [kLongPressTimeout] but not necessarily lifted. Not called for a /// double-tap-hold, which calls [onDoubleTapDown] instead. final GestureLongPressStartCallback? onSingleLongTapStart; /// Called after [onSingleLongTapStart] when the pointer is dragged. final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; /// Called after [onSingleLongTapStart] when the pointer is lifted. final GestureLongPressEndCallback? onSingleLongTapEnd; /// Called after [onSingleLongTapStart] when the pointer is canceled. final GestureLongPressCancelCallback? onSingleLongTapCancel; /// Called after a momentary hold or a short tap that is close in space and /// time (within [kDoubleTapTimeout]) to a previous short tap. final GestureTapDragDownCallback? onDoubleTapDown; /// Called after a momentary hold or a short tap that is close in space and /// time (within [kDoubleTapTimeout]) to a previous double-tap. final GestureTapDragDownCallback? onTripleTapDown; /// Called when a mouse starts dragging to select text. final GestureTapDragStartCallback? onDragSelectionStart; /// Called repeatedly as a mouse moves while dragging. final GestureTapDragUpdateCallback? onDragSelectionUpdate; /// Called when a mouse that was previously dragging is released. final GestureTapDragEndCallback? onDragSelectionEnd; /// Whether [onUserTap] will be called for all taps including consecutive taps. /// /// Defaults to false, so [onUserTap] is only called for each distinct tap. final bool onUserTapAlwaysCalled; /// How this gesture detector should behave during hit testing. /// /// This defaults to [HitTestBehavior.deferToChild]. final HitTestBehavior? behavior; /// Child below this widget. final Widget child; @override State createState() => _TextSelectionGestureDetectorState(); } class _TextSelectionGestureDetectorState extends State { // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, // which can grow to be infinitely large, to a value between 1 and 3. The value // that the raw count is converted to is based on the default observed behavior // on the native platforms. // // This method should be used in all instances when details.consecutiveTapCount // would be used. static int _getEffectiveConsecutiveTapCount(int rawCount) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: // From observation, these platform's reset their tap count to 0 when // the number of consecutive taps exceeds 3. For example on Debian Linux // with GTK, when going past a triple click, on the fourth click the // selection is moved to the precise click position, on the fifth click // the word at the position is selected, and on the sixth click the // paragraph at the position is selected. return rawCount <= 3 ? rawCount : (rawCount % 3 == 0 ? 3 : rawCount % 3); case TargetPlatform.iOS: case TargetPlatform.macOS: // From observation, these platform's either hold their tap count at 3. // For example on macOS, when going past a triple click, the selection // should be retained at the paragraph that was first selected on triple // click. return math.min(rawCount, 3); case TargetPlatform.windows: // From observation, this platform's consecutive tap actions alternate // between double click and triple click actions. For example, after a // triple click has selected a paragraph, on the next click the word at // the clicked position will be selected, and on the next click the // paragraph at the position is selected. return rawCount < 2 ? rawCount : 2 + rawCount % 2; } } void _handleTapTrackStart() { widget.onTapTrackStart?.call(); } void _handleTapTrackReset() { widget.onTapTrackReset?.call(); } // The down handler is force-run on success of a single tap and optimistically // run before a long press success. void _handleTapDown(TapDragDownDetails details) { widget.onTapDown?.call(details); // This isn't detected as a double tap gesture in the gesture recognizer // because it's 2 single taps, each of which may do different things depending // on whether it's a single tap, the first tap of a double tap, the second // tap held down, a clean double tap etc. if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) { return widget.onDoubleTapDown?.call(details); } if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) { return widget.onTripleTapDown?.call(details); } } void _handleTapUp(TapDragUpDetails details) { if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) { widget.onSingleTapUp?.call(details); widget.onUserTap?.call(); } else if (widget.onUserTapAlwaysCalled) { widget.onUserTap?.call(); } } void _handleTapCancel() { widget.onSingleTapCancel?.call(); } void _handleDragStart(TapDragStartDetails details) { widget.onDragSelectionStart?.call(details); } void _handleDragUpdate(TapDragUpdateDetails details) { widget.onDragSelectionUpdate?.call(details); } void _handleDragEnd(TapDragEndDetails details) { widget.onDragSelectionEnd?.call(details); } void _forcePressStarted(ForcePressDetails details) { widget.onForcePressStart?.call(details); } void _forcePressEnded(ForcePressDetails details) { widget.onForcePressEnd?.call(details); } void _handleLongPressStart(LongPressStartDetails details) { widget.onSingleLongTapStart?.call(details); } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { widget.onSingleLongTapMoveUpdate?.call(details); } void _handleLongPressEnd(LongPressEndDetails details) { widget.onSingleLongTapEnd?.call(details); } void _handleLongPressCancel() { widget.onSingleLongTapCancel?.call(); } @override Widget build(BuildContext context) { final Map gestures = {}; gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { instance ..onSecondaryTap = widget.onSecondaryTap ..onSecondaryTapDown = widget.onSecondaryTapDown; }, ); if (widget.onSingleLongTapStart != null || widget.onSingleLongTapMoveUpdate != null || widget.onSingleLongTapEnd != null || widget.onSingleLongTapCancel != null) { gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( debugOwner: this, supportedDevices: {PointerDeviceKind.touch}, ), (LongPressGestureRecognizer instance) { instance ..onLongPressStart = _handleLongPressStart ..onLongPressMoveUpdate = _handleLongPressMoveUpdate ..onLongPressEnd = _handleLongPressEnd ..onLongPressCancel = _handleLongPressCancel; }, ); } if (widget.onDragSelectionStart != null || widget.onDragSelectionUpdate != null || widget.onDragSelectionEnd != null) { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: gestures[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers< TapAndHorizontalDragGestureRecognizer >( () => TapAndHorizontalDragGestureRecognizer(debugOwner: this), (TapAndHorizontalDragGestureRecognizer instance) { instance // Text selection should start from the position of the first pointer // down event. ..dragStartBehavior = DragStartBehavior.down ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS ..onTapTrackStart = _handleTapTrackStart ..onTapTrackReset = _handleTapTrackReset ..onTapDown = _handleTapDown ..onDragStart = _handleDragStart ..onDragUpdate = _handleDragUpdate ..onDragEnd = _handleDragEnd ..onTapUp = _handleTapUp ..onCancel = _handleTapCancel; }, ); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: gestures[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapAndPanGestureRecognizer(debugOwner: this), (TapAndPanGestureRecognizer instance) { instance // Text selection should start from the position of the first pointer // down event. ..dragStartBehavior = DragStartBehavior.down ..onTapTrackStart = _handleTapTrackStart ..onTapTrackReset = _handleTapTrackReset ..onTapDown = _handleTapDown ..onDragStart = _handleDragStart ..onDragUpdate = _handleDragUpdate ..onDragEnd = _handleDragEnd ..onTapUp = _handleTapUp ..onCancel = _handleTapCancel; }, ); } } if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => ForcePressGestureRecognizer(debugOwner: this), (ForcePressGestureRecognizer instance) { instance ..onStart = widget.onForcePressStart != null ? _forcePressStarted : null ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null; }, ); } return RawGestureDetector( gestures: gestures, excludeFromSemantics: true, behavior: widget.behavior, child: widget.child, ); } } /// An object that manages a pair of text selection handles for a /// [RenderEditable]. /// /// This class is a wrapper of [SelectionOverlay] to provide APIs specific for /// [RenderEditable]s. To manage selection handles for custom widgets, use /// [SelectionOverlay] instead. class TextSelectionOverlay { /// Creates an object that manages overlay entries for selection handles. /// /// The [context] must have an [Overlay] as an ancestor. TextSelectionOverlay({ required TextEditingValue value, required this.context, Widget? debugRequiredFor, required LayerLink toolbarLayerLink, required LayerLink startHandleLayerLink, required LayerLink endHandleLayerLink, required this.renderObject, this.selectionControls, bool handlesVisible = false, required this.selectionDelegate, DragStartBehavior dragStartBehavior = DragStartBehavior.start, VoidCallback? onSelectionHandleTapped, ClipboardStatusNotifier? clipboardStatus, this.contextMenuBuilder, required TextMagnifierConfiguration magnifierConfiguration, required this.controller, }) : _handlesVisible = handlesVisible, _value = value { assert(debugMaybeDispatchCreated('widgets', 'TextSelectionOverlay', this)); renderObject.selectionStartInViewport.addListener( _updateTextSelectionOverlayVisibilities, ); renderObject.selectionEndInViewport.addListener( _updateTextSelectionOverlayVisibilities, ); _updateTextSelectionOverlayVisibilities(); _selectionOverlay = SelectionOverlay( magnifierConfiguration: magnifierConfiguration, context: context, debugRequiredFor: debugRequiredFor, // The metrics will be set when show handles. startHandleType: TextSelectionHandleType.collapsed, startHandlesVisible: _effectiveStartHandleVisibility, lineHeightAtStart: 0.0, onStartHandleDragStart: _handleSelectionStartHandleDragStart, onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate, onEndHandleDragEnd: _handleAnyDragEnd, endHandleType: TextSelectionHandleType.collapsed, endHandlesVisible: _effectiveEndHandleVisibility, lineHeightAtEnd: 0.0, onEndHandleDragStart: _handleSelectionEndHandleDragStart, onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate, onStartHandleDragEnd: _handleAnyDragEnd, toolbarVisible: _effectiveToolbarVisibility, selectionEndpoints: const [], selectionControls: selectionControls, selectionDelegate: selectionDelegate, clipboardStatus: clipboardStatus, startHandleLayerLink: startHandleLayerLink, endHandleLayerLink: endHandleLayerLink, toolbarLayerLink: toolbarLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, dragStartBehavior: dragStartBehavior, toolbarLocation: renderObject.lastSecondaryTapDownPosition, ); } final RichTextEditingController controller; /// {@template flutter.widgets.SelectionOverlay.context} /// The context in which the selection UI should appear. /// /// This context must have an [Overlay] as an ancestor because this object /// will display the text selection handles in that [Overlay]. /// {@endtemplate} final BuildContext context; // TODO(mpcomplete): what if the renderObject is removed or replaced, or // moves? Not sure what cases I need to handle, or how to handle them. /// The editable line in which the selected text is being displayed. final RenderEditable renderObject; /// {@macro flutter.widgets.SelectionOverlay.selectionControls} final TextSelectionControls? selectionControls; /// {@macro flutter.widgets.SelectionOverlay.selectionDelegate} final TextSelectionDelegate selectionDelegate; late final SelectionOverlay _selectionOverlay; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} /// /// If not provided, no context menu will be built. final WidgetBuilder? contextMenuBuilder; /// Retrieve current value. @visibleForTesting TextEditingValue get value => _value; TextEditingValue _value; TextSelection get _selection => _value.selection; final ValueNotifier _effectiveStartHandleVisibility = ValueNotifier(false); final ValueNotifier _effectiveEndHandleVisibility = ValueNotifier( false, ); final ValueNotifier _effectiveToolbarVisibility = ValueNotifier( false, ); void _updateTextSelectionOverlayVisibilities() { _effectiveStartHandleVisibility.value = _handlesVisible && renderObject.selectionStartInViewport.value; _effectiveEndHandleVisibility.value = _handlesVisible && renderObject.selectionEndInViewport.value; _effectiveToolbarVisibility.value = renderObject.selectionStartInViewport.value || renderObject.selectionEndInViewport.value; } /// Whether selection handles are visible. /// /// Set to false if you want to hide the handles. Use this property to show or /// hide the handle without rebuilding them. /// /// Defaults to false. bool get handlesVisible => _handlesVisible; bool _handlesVisible = false; set handlesVisible(bool visible) { if (_handlesVisible == visible) { return; } _handlesVisible = visible; _updateTextSelectionOverlayVisibilities(); } /// {@macro flutter.widgets.SelectionOverlay.showHandles} void showHandles() { _updateSelectionOverlay(); _selectionOverlay.showHandles(); } /// {@macro flutter.widgets.SelectionOverlay.hideHandles} void hideHandles() => _selectionOverlay.hideHandles(); /// {@macro flutter.widgets.SelectionOverlay.showToolbar} void showToolbar() { _updateSelectionOverlay(); if (selectionControls != null && selectionControls is! TextSelectionHandleControls) { _selectionOverlay.showToolbar(); return; } if (contextMenuBuilder == null) { return; } assert(context.mounted); _selectionOverlay.showToolbar( context: context, contextMenuBuilder: contextMenuBuilder, ); return; } /// Shows toolbar with spell check suggestions of misspelled words that are /// available for click-and-replace. void showSpellCheckSuggestionsToolbar( WidgetBuilder spellCheckSuggestionsToolbarBuilder, ) { _updateSelectionOverlay(); assert(context.mounted); _selectionOverlay.showSpellCheckSuggestionsToolbar( context: context, builder: spellCheckSuggestionsToolbarBuilder, ); hideHandles(); } /// {@macro flutter.widgets.SelectionOverlay.showMagnifier} void showMagnifier(Offset positionToShow) { final TextPosition position = renderObject.getPositionForPoint( positionToShow, ); _updateSelectionOverlay(); _selectionOverlay.showMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: positionToShow, renderEditable: renderObject, ), ); } /// {@macro flutter.widgets.SelectionOverlay.updateMagnifier} void updateMagnifier(Offset positionToShow) { final TextPosition position = renderObject.getPositionForPoint( positionToShow, ); _updateSelectionOverlay(); _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: positionToShow, renderEditable: renderObject, ), ); } /// {@macro flutter.widgets.SelectionOverlay.hideMagnifier} void hideMagnifier() { _selectionOverlay.hideMagnifier(); } /// Updates the overlay after the selection has changed. /// /// If this method is called while the [SchedulerBinding.schedulerPhase] is /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed /// until the post-frame callbacks phase. Otherwise the update is done /// synchronously. This means that it is safe to call during builds, but also /// that if you do call this during a build, the UI will not update until the /// next frame (i.e. many milliseconds later). void update(TextEditingValue newValue) { if (_value == newValue) { return; } _value = newValue; _updateSelectionOverlay(); // _updateSelectionOverlay may not rebuild the selection overlay if the // text metrics and selection doesn't change even if the text has changed. // This rebuild is needed for the toolbar to update based on the latest text // value. _selectionOverlay.markNeedsBuild(); } void _updateSelectionOverlay() { _selectionOverlay // Update selection handle metrics. ..startHandleType = _chooseType( renderObject.textDirection, TextSelectionHandleType.left, TextSelectionHandleType.right, ) ..lineHeightAtStart = _getStartGlyphHeight() ..endHandleType = _chooseType( renderObject.textDirection, TextSelectionHandleType.right, TextSelectionHandleType.left, ) ..lineHeightAtEnd = _getEndGlyphHeight() // Update selection toolbar metrics. ..selectionEndpoints = renderObject.getEndpointsForSelection(_selection) ..toolbarLocation = renderObject.lastSecondaryTapDownPosition; } /// Causes the overlay to update its rendering. /// /// This is intended to be called when the [renderObject] may have changed its /// text metrics (e.g. because the text was scrolled). void updateForScroll() { _updateSelectionOverlay(); // This method may be called due to windows metrics changes. In that case, // non of the properties in _selectionOverlay will change, but a rebuild is // still needed. _selectionOverlay.markNeedsBuild(); } /// Whether the handles are currently visible. bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible; /// {@macro flutter.widgets.SelectionOverlay.toolbarIsVisible} /// /// See also: /// /// * [spellCheckToolbarIsVisible], which is only whether the spell check menu /// specifically is visible. bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible; /// {@macro flutter.widgets.SelectionOverlay.magnifierIsVisible} bool get magnifierIsVisible => _selectionOverlay.magnifierIsVisible; /// {@macro flutter.widgets.SelectionOverlay.magnifierExists} bool get magnifierExists => _selectionOverlay.magnifierExists; /// Whether the spell check menu is currently visible. /// /// See also: /// /// * [toolbarIsVisible], which is whether any toolbar is visible. bool get spellCheckToolbarIsVisible => _selectionOverlay._spellCheckToolbarController.isShown; /// {@macro flutter.widgets.SelectionOverlay.hide} void hide() => _selectionOverlay.hide(); /// {@macro flutter.widgets.SelectionOverlay.hideToolbar} void hideToolbar() => _selectionOverlay.hideToolbar(); /// {@macro flutter.widgets.SelectionOverlay.dispose} void dispose() { assert(debugMaybeDispatchDisposed(this)); _selectionOverlay.dispose(); renderObject.selectionStartInViewport.removeListener( _updateTextSelectionOverlayVisibilities, ); renderObject.selectionEndInViewport.removeListener( _updateTextSelectionOverlayVisibilities, ); _effectiveToolbarVisibility.dispose(); _effectiveStartHandleVisibility.dispose(); _effectiveEndHandleVisibility.dispose(); hideToolbar(); } double _getStartGlyphHeight() { final String currText = selectionDelegate.textEditingValue.text; final int firstSelectedGraphemeExtent; Rect? startHandleRect; // Only calculate handle rects if the text in the previous frame // is the same as the text in the current frame. This is done because // widget.renderObject contains the renderEditable from the previous frame. // If the text changed between the current and previous frames then // widget.renderObject.getRectForComposingRange might fail. In cases where // the current frame is different from the previous we fall back to // renderObject.preferredLineHeight. if (renderObject.plainText == currText && _selection.isValid && !_selection.isCollapsed) { final String selectedGraphemes = _selection.textInside(currText); firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length; startHandleRect = renderObject.getRectForComposingRange( TextRange( start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent, ), ); } return startHandleRect?.height ?? renderObject.preferredLineHeight; } double _getEndGlyphHeight() { final String currText = selectionDelegate.textEditingValue.text; final int lastSelectedGraphemeExtent; Rect? endHandleRect; // See the explanation in _getStartGlyphHeight. if (renderObject.plainText == currText && _selection.isValid && !_selection.isCollapsed) { final String selectedGraphemes = _selection.textInside(currText); lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length; endHandleRect = renderObject.getRectForComposingRange( TextRange( start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end, ), ); } return endHandleRect?.height ?? renderObject.preferredLineHeight; } MagnifierInfo _buildMagnifier({ required RenderEditable renderEditable, required Offset globalGesturePosition, required TextPosition currentTextPosition, }) { final TextSelection lineAtOffset = renderEditable.getLineAtOffset( currentTextPosition, ); final TextPosition positionAtEndOfLine = TextPosition( offset: lineAtOffset.extentOffset, affinity: TextAffinity.upstream, ); // Default affinity is downstream. final TextPosition positionAtBeginningOfLine = TextPosition( offset: lineAtOffset.baseOffset, ); final Rect localLineBoundaries = Rect.fromPoints( renderEditable.getLocalRectForCaret(positionAtBeginningOfLine).topCenter, renderEditable.getLocalRectForCaret(positionAtEndOfLine).bottomCenter, ); final RenderBox? overlay = Overlay.of(context, rootOverlay: true).context.findRenderObject() as RenderBox?; final Matrix4 transformToOverlay = renderEditable.getTransformTo(overlay); final Rect overlayLineBoundaries = MatrixUtils.transformRect( transformToOverlay, localLineBoundaries, ); final Rect localCaretRect = renderEditable.getLocalRectForCaret( currentTextPosition, ); final Rect overlayCaretRect = MatrixUtils.transformRect( transformToOverlay, localCaretRect, ); final Offset overlayGesturePosition = overlay?.globalToLocal(globalGesturePosition) ?? globalGesturePosition; return MagnifierInfo( fieldBounds: MatrixUtils.transformRect( transformToOverlay, renderEditable.paintBounds, ), globalGesturePosition: overlayGesturePosition, caretRect: overlayCaretRect, currentLineBoundaries: overlayLineBoundaries, ); } // The contact position of the gesture at the current end handle location, in // global coordinates. Updated when the handle moves. late double _endHandleDragPosition; // The distance from _endHandleDragPosition to the center of the line that it // corresponds to, in global coordinates. late double _endHandleDragTarget; // The initial selection when a selection handle drag has started. // // This is used on Apple platforms to: // // 1. Preserve a collapsed selection: if the selection was collapsed when the drag // began, then it should remain collapsed throughout the entire drag. // 2. Anchor the non-dragged end of a non-collapsed selection: On Apple platforms, // the dragged handle always defines the selection's new extent. The drag start // selection provides the original position for the selection's new base. This // allows the selection handles to correctly swap their logical order (invert) // during the drag. TextSelection? _dragStartSelection; void _handleSelectionEndHandleDragStart(DragStartDetails details) { if (!renderObject.attached) { return; } _endHandleDragPosition = details.globalPosition.dy; // Use local coordinates when dealing with line height. because in case of a // scale transformation, the line height will also be scaled. final double centerOfLineLocal = _selectionOverlay.selectionEndpoints.last.point.dy - renderObject.preferredLineHeight / 2; final double centerOfLineGlobal = renderObject .localToGlobal(Offset(0.0, centerOfLineLocal)) .dy; _endHandleDragTarget = centerOfLineGlobal - details.globalPosition.dy; // Instead of finding the TextPosition at the handle's location directly, // use the vertical center of the line that it points to. This is because // selection handles typically hang above or below the line that they point // to. final TextPosition position = renderObject.getPositionForPoint( Offset(details.globalPosition.dx, centerOfLineGlobal), ); // The drag start selection is only utilized on Apple platforms. if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { _dragStartSelection ??= _selection; } _selectionOverlay.showMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); } /// Given a handle position and drag position, returns the position of handle /// after the drag. /// /// The handle jumps instantly between lines when the drag reaches a full /// line's height away from the original handle position. In other words, the /// line jump happens when the contact point would be located at the same /// place on the handle at the new line as when the gesture started, for both /// directions. /// /// This is not the same as just maintaining an offset from the target and the /// contact point. There is no point at which moving the drag up and down a /// small sub-line-height distance will cause the cursor to jump up and down /// between lines. The drag distance must be a full line height for the cursor /// to change lines, for both directions. /// /// Both parameters must be in local coordinates because the untransformed /// line height is used, and the return value is in local coordinates as well. double _getHandleDy(double dragDy, double handleDy) { final double distanceDragged = dragDy - handleDy; final int dragDirection = distanceDragged < 0.0 ? -1 : 1; final int linesDragged = dragDirection * (distanceDragged.abs() / renderObject.preferredLineHeight).floor(); return handleDy + linesDragged * renderObject.preferredLineHeight; } void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { if (!renderObject.attached) { return; } // This is NOT the same as details.localPosition. That is relative to the // selection handle, whereas this is relative to the RenderEditable. final Offset localPosition = renderObject.globalToLocal( details.globalPosition, ); final double nextEndHandleDragPositionLocal = _getHandleDy( localPosition.dy, renderObject.globalToLocal(Offset(0.0, _endHandleDragPosition)).dy, ); _endHandleDragPosition = renderObject .localToGlobal(Offset(0.0, nextEndHandleDragPositionLocal)) .dy; final Offset handleTargetGlobal = Offset( details.globalPosition.dx, _endHandleDragPosition + _endHandleDragTarget, ); TextPosition position = renderObject.getPositionForPoint( handleTargetGlobal, ); // bggRGjQaUbCoE right drag position = controller.dragOffset(position); final TextSelection newSelection; switch (defaultTargetPlatform) { // On Apple platforms, dragging the base handle makes it the extent. case TargetPlatform.iOS: case TargetPlatform.macOS: assert(_dragStartSelection != null); if (_dragStartSelection!.isCollapsed) { _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); final TextSelection currentSelection = TextSelection.fromPosition( position, ); _handleSelectionHandleChanged(currentSelection); return; } // Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized // always returns true for a TextSelection. final bool dragStartSelectionNormalized = _dragStartSelection!.extentOffset >= _dragStartSelection!.baseOffset; newSelection = TextSelection( baseOffset: dragStartSelectionNormalized ? _dragStartSelection!.baseOffset : _dragStartSelection!.extentOffset, extentOffset: position.offset, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: if (_selection.isCollapsed) { _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); final TextSelection currentSelection = TextSelection.fromPosition( position, ); _handleSelectionHandleChanged(currentSelection); return; } newSelection = TextSelection( baseOffset: _selection.baseOffset, extentOffset: position.offset, ); if (newSelection.baseOffset >= newSelection.extentOffset) { return; // Don't allow order swapping. } } _handleSelectionHandleChanged(newSelection); _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: newSelection.extent, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); } // The contact position of the gesture at the current start handle location, // in global coordinates. Updated when the handle moves. late double _startHandleDragPosition; // The distance from _startHandleDragPosition to the center of the line that // it corresponds to, in global coordinates. late double _startHandleDragTarget; void _handleSelectionStartHandleDragStart(DragStartDetails details) { if (!renderObject.attached) { return; } _startHandleDragPosition = details.globalPosition.dy; // Use local coordinates when dealing with line height. because in case of a // scale transformation, the line height will also be scaled. final double centerOfLineLocal = _selectionOverlay.selectionEndpoints.first.point.dy - renderObject.preferredLineHeight / 2; final double centerOfLineGlobal = renderObject .localToGlobal(Offset(0.0, centerOfLineLocal)) .dy; _startHandleDragTarget = centerOfLineGlobal - details.globalPosition.dy; // Instead of finding the TextPosition at the handle's location directly, // use the vertical center of the line that it points to. This is because // selection handles typically hang above or below the line that they point // to. final TextPosition position = renderObject.getPositionForPoint( Offset(details.globalPosition.dx, centerOfLineGlobal), ); // The drag start selection is only utilized on Apple platforms. if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { _dragStartSelection ??= _selection; } _selectionOverlay.showMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); } void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) { if (!renderObject.attached) { return; } // This is NOT the same as details.localPosition. That is relative to the // selection handle, whereas this is relative to the RenderEditable. final Offset localPosition = renderObject.globalToLocal( details.globalPosition, ); final double nextStartHandleDragPositionLocal = _getHandleDy( localPosition.dy, renderObject.globalToLocal(Offset(0.0, _startHandleDragPosition)).dy, ); _startHandleDragPosition = renderObject .localToGlobal(Offset(0.0, nextStartHandleDragPositionLocal)) .dy; final Offset handleTargetGlobal = Offset( details.globalPosition.dx, _startHandleDragPosition + _startHandleDragTarget, ); TextPosition position = renderObject.getPositionForPoint( handleTargetGlobal, ); // bggRGjQaUbCoE single drag, left drag position = controller.dragOffset(position); final TextSelection newSelection; switch (defaultTargetPlatform) { // On Apple platforms, dragging the base handle makes it the extent. case TargetPlatform.iOS: case TargetPlatform.macOS: assert(_dragStartSelection != null); if (_dragStartSelection!.isCollapsed) { _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); final TextSelection currentSelection = TextSelection.fromPosition( position, ); _handleSelectionHandleChanged(currentSelection); return; } // Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized // always returns true for a TextSelection. final bool dragStartSelectionNormalized = _dragStartSelection!.extentOffset >= _dragStartSelection!.baseOffset; newSelection = TextSelection( baseOffset: dragStartSelectionNormalized ? _dragStartSelection!.extentOffset : _dragStartSelection!.baseOffset, extentOffset: position.offset, ); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: if (_selection.isCollapsed) { _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: position, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); final TextSelection currentSelection = TextSelection.fromPosition( position, ); _handleSelectionHandleChanged(currentSelection); return; } newSelection = TextSelection( baseOffset: position.offset, extentOffset: _selection.extentOffset, ); if (newSelection.baseOffset >= newSelection.extentOffset) { return; // Don't allow order swapping. } } _selectionOverlay.updateMagnifier( _buildMagnifier( currentTextPosition: newSelection.extent.offset < newSelection.base.offset ? newSelection.extent : newSelection.base, globalGesturePosition: details.globalPosition, renderEditable: renderObject, ), ); _handleSelectionHandleChanged(newSelection); } void _handleAnyDragEnd(DragEndDetails details) { if (!context.mounted) { return; } _dragStartSelection = null; final bool draggingHandles = _selectionOverlay.isDraggingStartHandle || _selectionOverlay.isDraggingEndHandle; if (selectionControls is! TextSelectionHandleControls) { if (!draggingHandles) { _selectionOverlay.hideMagnifier(); if (!_selection.isCollapsed) { _selectionOverlay.showToolbar(); } } return; } if (!draggingHandles) { _selectionOverlay.hideMagnifier(); if (!_selection.isCollapsed) { _selectionOverlay.showToolbar( context: context, contextMenuBuilder: contextMenuBuilder, ); } } } void _handleSelectionHandleChanged(TextSelection newSelection) { selectionDelegate.userUpdateTextEditingValue( _value.copyWith(selection: newSelection), SelectionChangedCause.drag, ); } TextSelectionHandleType _chooseType( TextDirection textDirection, TextSelectionHandleType ltrType, TextSelectionHandleType rtlType, ) { if (_selection.isCollapsed) { return TextSelectionHandleType.collapsed; } return switch (textDirection) { TextDirection.ltr => ltrType, TextDirection.rtl => rtlType, }; } } /// An object that manages a pair of selection handles and a toolbar. /// /// The selection handles are displayed in the [Overlay] that most closely /// encloses the given [BuildContext]. class SelectionOverlay { /// Creates an object that manages overlay entries for selection handles. /// /// The [context] must have an [Overlay] as an ancestor. SelectionOverlay({ required this.context, this.debugRequiredFor, required TextSelectionHandleType startHandleType, required double lineHeightAtStart, this.startHandlesVisible, this.onStartHandleDragStart, this.onStartHandleDragUpdate, this.onStartHandleDragEnd, required TextSelectionHandleType endHandleType, required double lineHeightAtEnd, this.endHandlesVisible, this.onEndHandleDragStart, this.onEndHandleDragUpdate, this.onEndHandleDragEnd, this.toolbarVisible, required List selectionEndpoints, required this.selectionControls, @Deprecated( 'Use `contextMenuBuilder` in `showToolbar` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) required this.selectionDelegate, required this.clipboardStatus, required this.startHandleLayerLink, required this.endHandleLayerLink, required this.toolbarLayerLink, this.dragStartBehavior = DragStartBehavior.start, this.onSelectionHandleTapped, @Deprecated( 'Use `contextMenuBuilder` in `showToolbar` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) Offset? toolbarLocation, this.magnifierConfiguration = TextMagnifierConfiguration.disabled, }) : _startHandleType = startHandleType, _lineHeightAtStart = lineHeightAtStart, _endHandleType = endHandleType, _lineHeightAtEnd = lineHeightAtEnd, _selectionEndpoints = selectionEndpoints, _toolbarLocation = toolbarLocation, assert(debugCheckHasOverlay(context)) { assert(debugMaybeDispatchCreated('widgets', 'SelectionOverlay', this)); } /// {@macro flutter.widgets.SelectionOverlay.context} final BuildContext context; final ValueNotifier _magnifierInfo = ValueNotifier( MagnifierInfo.empty, ); // [MagnifierController.show] and [MagnifierController.hide] should not be // called directly, except from inside [showMagnifier] and [hideMagnifier]. If // it is desired to show or hide the magnifier, call [showMagnifier] or // [hideMagnifier]. This is because the magnifier needs to orchestrate with // other properties in [SelectionOverlay]. final MagnifierController _magnifierController = MagnifierController(); /// The configuration for the magnifier. /// /// By default, [SelectionOverlay]'s [TextMagnifierConfiguration] is disabled. /// /// {@macro flutter.widgets.magnifier.intro} final TextMagnifierConfiguration magnifierConfiguration; /// {@template flutter.widgets.SelectionOverlay.toolbarIsVisible} /// Whether the toolbar is currently visible. /// /// Includes both the text selection toolbar and the spell check menu. /// {@endtemplate} bool get toolbarIsVisible { return selectionControls is TextSelectionHandleControls ? _contextMenuController.isShown || _spellCheckToolbarController.isShown : _toolbar != null || _spellCheckToolbarController.isShown; } /// {@template flutter.widgets.SelectionOverlay.magnifierIsVisible} /// Whether the magnifier is currently visible. /// {@endtemplate} bool get magnifierIsVisible => _magnifierController.shown; /// {@template flutter.widgets.SelectionOverlay.magnifierExists} /// Whether the magnifier currently exists. /// /// This differs from [magnifierIsVisible] in that the magnifier may exist /// in the overlay, but not be shown. /// {@endtemplate} bool get magnifierExists => _magnifierController.overlayEntry != null; /// {@template flutter.widgets.SelectionOverlay.showMagnifier} /// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier] /// was called. This is safe to call on platforms not mobile, since /// a magnifierBuilder will not be provided, or the magnifierBuilder will return null /// on platforms not mobile. /// /// This is NOT the source of truth for if the magnifier is up or not, /// since magnifiers may hide themselves. If this info is needed, check /// [MagnifierController.shown]. /// {@endtemplate} void showMagnifier(MagnifierInfo initialMagnifierInfo) { // Do not show the magnifier if one already exists. if (_magnifierController.overlayEntry != null) { return; } if (toolbarIsVisible) { hideToolbar(); } // Start from empty, so we don't utilize any remnant values. _magnifierInfo.value = initialMagnifierInfo; // Pre-build the magnifiers so we can tell if we've built something // or not. If we don't build a magnifiers, then we should not // insert anything in the overlay. final Widget? builtMagnifier = magnifierConfiguration.magnifierBuilder( context, _magnifierController, _magnifierInfo, ); if (builtMagnifier == null) { return; } _magnifierController.show( context: context, below: magnifierConfiguration.shouldDisplayHandlesInMagnifier ? null : _handles?.start, builder: (_) => builtMagnifier, ); } /// {@template flutter.widgets.SelectionOverlay.hideMagnifier} /// Hide the current magnifier. /// /// This does nothing if there is no magnifier. /// {@endtemplate} void hideMagnifier() { // This cannot be a check on `MagnifierController.shown`, since // it's possible that the magnifier is still in the overlay, but // not shown in cases where the magnifier hides itself. if (_magnifierController.overlayEntry == null) { return; } _magnifierController.hide(); } /// The type of start selection handle. /// /// Changing the value while the handles are visible causes them to rebuild. TextSelectionHandleType get startHandleType => _startHandleType; TextSelectionHandleType _startHandleType; set startHandleType(TextSelectionHandleType value) { if (_startHandleType == value) { return; } _startHandleType = value; markNeedsBuild(); } /// The line height at the selection start. /// /// This value is used for calculating the size of the start selection handle. /// /// Changing the value while the handles are visible causes them to rebuild. double get lineHeightAtStart => _lineHeightAtStart; double _lineHeightAtStart; set lineHeightAtStart(double value) { if (_lineHeightAtStart == value) { return; } _lineHeightAtStart = value; markNeedsBuild(); } // Whether a drag is in progress on the start handle. This differs from // `_isDraggingStartHandle` in that it is not blocked by `_canDragStartHandle`. bool _startHandleDragInProgress = false; /// Whether the selection start handle is currently being dragged. bool get isDraggingStartHandle => _isDraggingStartHandle || _startHandleDragInProgress; bool _isDraggingStartHandle = false; // Whether the start handle can be dragged. // // On Apple and web platforms only one selection handle can be dragged // at a time, so when the end handle is being dragged on these platforms // the the start handle cannot be dragged. bool get _canDragStartHandle => !_isDraggingEndHandle || (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.macOS && !kIsWeb); /// Whether the start handle is visible. /// /// If the value changes, the start handle uses [FadeTransition] to transition /// itself on and off the screen. /// /// If this is null, the start selection handle will always be visible. final ValueListenable? startHandlesVisible; /// Called when the users start dragging the start selection handles. final ValueChanged? onStartHandleDragStart; void _handleStartHandleDragStart(DragStartDetails details) { assert(!_isDraggingStartHandle); // Calling OverlayEntry.remove may not happen until the following frame, so // it's possible for the handles to receive a gesture after calling remove. if (_handles == null) { _isDraggingStartHandle = false; return; } _startHandleDragInProgress = true; if (!_canDragStartHandle) { return; } _isDraggingStartHandle = details.kind == PointerDeviceKind.touch; onStartHandleDragStart?.call(details); } void _handleStartHandleDragUpdate(DragUpdateDetails details) { // Calling OverlayEntry.remove may not happen until the following frame, so // it's possible for the handles to receive a gesture after calling remove. if (_handles == null) { _isDraggingStartHandle = false; return; } if (!_canDragStartHandle) { return; } // The handle drag may have been blocked before on Apple platforms and the web // while the opposite handle was being dragged. Ensure that any logic that was // meant to be run in onStartHandleDragStart is still run. if (!_isDraggingStartHandle) { _isDraggingStartHandle = details.kind == PointerDeviceKind.touch; final DragStartDetails startDetails = DragStartDetails( globalPosition: details.globalPosition, localPosition: details.localPosition, sourceTimeStamp: details.sourceTimeStamp, kind: details.kind, ); onStartHandleDragStart?.call(startDetails); } onStartHandleDragUpdate?.call(details); } /// Called when the users drag the start selection handles to new locations. final ValueChanged? onStartHandleDragUpdate; /// Called when the users lift their fingers after dragging the start selection /// handles. final ValueChanged? onStartHandleDragEnd; void _handleStartHandleDragEnd(DragEndDetails details) { _isDraggingStartHandle = false; // Calling OverlayEntry.remove may not happen until the following frame, so // it's possible for the handles to receive a gesture after calling remove. if (_handles == null) { return; } _startHandleDragInProgress = false; if (!_canDragStartHandle) { return; } onStartHandleDragEnd?.call(details); } /// The type of end selection handle. /// /// Changing the value while the handles are visible causes them to rebuild. TextSelectionHandleType get endHandleType => _endHandleType; TextSelectionHandleType _endHandleType; set endHandleType(TextSelectionHandleType value) { if (_endHandleType == value) { return; } _endHandleType = value; markNeedsBuild(); } /// The line height at the selection end. /// /// This value is used for calculating the size of the end selection handle. /// /// Changing the value while the handles are visible causes them to rebuild. double get lineHeightAtEnd => _lineHeightAtEnd; double _lineHeightAtEnd; set lineHeightAtEnd(double value) { if (_lineHeightAtEnd == value) { return; } _lineHeightAtEnd = value; markNeedsBuild(); } // Whether a drag is in progress on the start handle. This differs from // `_isDraggingEndHandle` in that it is not blocked by `_canDragEndHandle`. bool _endHandleDragInProgress = false; /// Whether the selection end handle is currently being dragged. bool get isDraggingEndHandle => _isDraggingEndHandle || _endHandleDragInProgress; bool _isDraggingEndHandle = false; // Whether the end handle can be dragged. // // On Apple and web platforms only one selection handle can be dragged // at a time, so when the start handle is being dragged on these platforms // the the end handle cannot be dragged. bool get _canDragEndHandle => !_isDraggingStartHandle || (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.macOS && !kIsWeb); /// Whether the end handle is visible. /// /// If the value changes, the end handle uses [FadeTransition] to transition /// itself on and off the screen. /// /// If this is null, the end selection handle will always be visible. final ValueListenable? endHandlesVisible; /// Called when the users start dragging the end selection handles. final ValueChanged? onEndHandleDragStart; void _handleEndHandleDragStart(DragStartDetails details) { assert(!_isDraggingEndHandle); // Calling OverlayEntry.remove may not happen until the following frame, so // it's possible for the handles to receive a gesture after calling remove. if (_handles == null) { _isDraggingEndHandle = false; return; } _endHandleDragInProgress = true; if (!_canDragEndHandle) { return; } _isDraggingEndHandle = details.kind == PointerDeviceKind.touch; onEndHandleDragStart?.call(details); } void _handleEndHandleDragUpdate(DragUpdateDetails details) { // Calling OverlayEntry.remove may not happen until the following frame, so // it's possible for the handles to receive a gesture after calling remove. if (_handles == null) { _isDraggingEndHandle = false; return; } if (!_canDragEndHandle) { return; } // The handle drag may have been blocked before on Apple platforms and the web // while the opposite handle was being dragged. Ensure that any logic that was // meant to be run in onStartHandleDragStart is still run. if (!_isDraggingEndHandle) { _isDraggingEndHandle = details.kind == PointerDeviceKind.touch; final DragStartDetails startDetails = DragStartDetails( globalPosition: details.globalPosition, localPosition: details.localPosition, sourceTimeStamp: details.sourceTimeStamp, kind: details.kind, ); onEndHandleDragStart?.call(startDetails); } onEndHandleDragUpdate?.call(details); } /// Called when the users drag the end selection handles to new locations. final ValueChanged? onEndHandleDragUpdate; /// Called when the users lift their fingers after dragging the end selection /// handles. final ValueChanged? onEndHandleDragEnd; void _handleEndHandleDragEnd(DragEndDetails details) { _isDraggingEndHandle = false; // Calling OverlayEntry.remove may not happen until the following frame, so // it's possible for the handles to receive a gesture after calling remove. if (_handles == null) { return; } _endHandleDragInProgress = false; if (!_canDragEndHandle) { return; } onEndHandleDragEnd?.call(details); } /// Whether the toolbar is visible. /// /// If the value changes, the toolbar uses [FadeTransition] to transition /// itself on and off the screen. /// /// If this is null the toolbar will always be visible. final ValueListenable? toolbarVisible; /// The text selection positions of selection start and end. List get selectionEndpoints => _selectionEndpoints; List _selectionEndpoints; set selectionEndpoints(List value) { if (!listEquals(_selectionEndpoints, value)) { markNeedsBuild(); if (_isDraggingEndHandle || _isDraggingStartHandle) { switch (defaultTargetPlatform) { case TargetPlatform.android: HapticFeedback.selectionClick(); case TargetPlatform.fuchsia: case TargetPlatform.iOS: case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: break; } } } _selectionEndpoints = value; } /// Debugging information for explaining why the [Overlay] is required. final Widget? debugRequiredFor; /// The object supplied to the [CompositedTransformTarget] that wraps the text /// field. final LayerLink toolbarLayerLink; /// The objects supplied to the [CompositedTransformTarget] that wraps the /// location of start selection handle. final LayerLink startHandleLayerLink; /// The objects supplied to the [CompositedTransformTarget] that wraps the /// location of end selection handle. final LayerLink endHandleLayerLink; /// {@template flutter.widgets.SelectionOverlay.selectionControls} /// Builds text selection handles and toolbar. /// {@endtemplate} final TextSelectionControls? selectionControls; /// {@template flutter.widgets.SelectionOverlay.selectionDelegate} /// The delegate for manipulating the current selection in the owning /// text field. /// {@endtemplate} @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) final TextSelectionDelegate? selectionDelegate; /// Determines the way that drag start behavior is handled. /// /// If set to [DragStartBehavior.start], handle drag behavior will /// begin at the position where the drag gesture won the arena. If set to /// [DragStartBehavior.down] it will begin at the position where a down /// event is first detected. /// /// In general, setting this to [DragStartBehavior.start] will make drag /// animation smoother and setting it to [DragStartBehavior.down] will make /// drag behavior feel slightly more reactive. /// /// By default, the drag start behavior is [DragStartBehavior.start]. /// /// See also: /// /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. final DragStartBehavior dragStartBehavior; /// {@template flutter.widgets.SelectionOverlay.onSelectionHandleTapped} /// A callback that's optionally invoked when a selection handle is tapped. /// /// The [TextSelectionControls.buildHandle] implementation the text field /// uses decides where the handle's tap "hotspot" is, or whether the /// selection handle supports tap gestures at all. For instance, /// [MaterialTextSelectionControls] calls [onSelectionHandleTapped] when the /// selection handle's "knob" is tapped, while /// [CupertinoTextSelectionControls] builds a handle that's not sufficiently /// large for tapping (as it's not meant to be tapped) so it does not call /// [onSelectionHandleTapped] even when tapped. /// {@endtemplate} // See https://github.com/flutter/flutter/issues/39376#issuecomment-848406415 // for provenance. final VoidCallback? onSelectionHandleTapped; /// Maintains the status of the clipboard for determining if its contents can /// be pasted or not. /// /// Useful because the actual value of the clipboard can only be checked /// asynchronously (see [Clipboard.getData]). final ClipboardStatusNotifier? clipboardStatus; /// The location of where the toolbar should be drawn in relative to the /// location of [toolbarLayerLink]. /// /// If this is null, the toolbar is drawn based on [selectionEndpoints] and /// the rect of render object of [context]. /// /// This is useful for displaying toolbars at the mouse right-click locations /// in desktop devices. @Deprecated( 'Use the `contextMenuBuilder` parameter in `showToolbar` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) Offset? get toolbarLocation => _toolbarLocation; Offset? _toolbarLocation; set toolbarLocation(Offset? value) { if (_toolbarLocation == value) { return; } _toolbarLocation = value; markNeedsBuild(); } /// Controls the fade-in and fade-out animations for the toolbar and handles. static const Duration fadeDuration = Duration(milliseconds: 150); /// A pair of handles. If this is non-null, there are always 2, though the /// second is hidden when the selection is collapsed. ({OverlayEntry start, OverlayEntry end})? _handles; /// A copy/paste toolbar. OverlayEntry? _toolbar; // Manages the context menu. Not necessarily visible when non-null. final ContextMenuController _contextMenuController = ContextMenuController(); final ContextMenuController _spellCheckToolbarController = ContextMenuController(); /// {@template flutter.widgets.SelectionOverlay.showHandles} /// Builds the handles by inserting them into the [context]'s overlay. /// {@endtemplate} void showHandles() { if (_handles != null) { return; } final OverlayState overlay = Overlay.of( context, rootOverlay: true, debugRequiredFor: debugRequiredFor, ); final CapturedThemes capturedThemes = InheritedTheme.capture( from: context, to: overlay.context, ); _handles = ( start: OverlayEntry( builder: (BuildContext context) { return capturedThemes.wrap(_buildStartHandle(context)); }, ), end: OverlayEntry( builder: (BuildContext context) { return capturedThemes.wrap(_buildEndHandle(context)); }, ), ); overlay.insertAll([_handles!.start, _handles!.end]); } /// {@template flutter.widgets.SelectionOverlay.hideHandles} /// Destroys the handles by removing them from overlay. /// {@endtemplate} void hideHandles() { if (_handles != null) { _handles!.start.remove(); _handles!.start.dispose(); _handles!.end.remove(); _handles!.end.dispose(); _handles = null; } } /// {@template flutter.widgets.SelectionOverlay.showToolbar} /// Shows the toolbar by inserting it into the [context]'s overlay. /// {@endtemplate} void showToolbar({BuildContext? context, WidgetBuilder? contextMenuBuilder}) { if (contextMenuBuilder == null) { if (_toolbar != null) { return; } _toolbar = OverlayEntry(builder: _buildToolbar); Overlay.of( this.context, rootOverlay: true, debugRequiredFor: debugRequiredFor, ).insert(_toolbar!); return; } if (context == null) { return; } final RenderBox renderBox = context.findRenderObject()! as RenderBox; _contextMenuController.show( context: context, contextMenuBuilder: (BuildContext context) { return _SelectionToolbarWrapper( visibility: toolbarVisible, layerLink: toolbarLayerLink, offset: -renderBox.localToGlobal(Offset.zero), child: contextMenuBuilder(context), ); }, ); } /// Shows toolbar with spell check suggestions of misspelled words that are /// available for click-and-replace. void showSpellCheckSuggestionsToolbar({ BuildContext? context, required WidgetBuilder builder, }) { if (context == null) { return; } final RenderBox renderBox = context.findRenderObject()! as RenderBox; _spellCheckToolbarController.show( context: context, contextMenuBuilder: (BuildContext context) { return _SelectionToolbarWrapper( layerLink: toolbarLayerLink, offset: -renderBox.localToGlobal(Offset.zero), child: builder(context), ); }, ); } bool _buildScheduled = false; /// Rebuilds the selection toolbar or handles if they are present. void markNeedsBuild() { if (_handles == null && _toolbar == null) { return; } // If we are in build state, it will be too late to update visibility. // We will need to schedule the build in next frame. if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { if (_buildScheduled) { return; } _buildScheduled = true; SchedulerBinding.instance.addPostFrameCallback((Duration duration) { _buildScheduled = false; _handles?.start.markNeedsBuild(); _handles?.end.markNeedsBuild(); _toolbar?.markNeedsBuild(); if (_contextMenuController.isShown) { _contextMenuController.markNeedsBuild(); } else if (_spellCheckToolbarController.isShown) { _spellCheckToolbarController.markNeedsBuild(); } }, debugLabel: 'SelectionOverlay.markNeedsBuild'); } else { if (_handles != null) { _handles!.start.markNeedsBuild(); _handles!.end.markNeedsBuild(); } _toolbar?.markNeedsBuild(); if (_contextMenuController.isShown) { _contextMenuController.markNeedsBuild(); } else if (_spellCheckToolbarController.isShown) { _spellCheckToolbarController.markNeedsBuild(); } } } /// {@template flutter.widgets.SelectionOverlay.hide} /// Hides the entire overlay including the toolbar and the handles. /// {@endtemplate} void hide() { _magnifierController.hide(); hideHandles(); if (_toolbar != null || _contextMenuController.isShown || _spellCheckToolbarController.isShown) { hideToolbar(); } } /// {@template flutter.widgets.SelectionOverlay.hideToolbar} /// Hides the toolbar part of the overlay. /// /// To hide the whole overlay, see [hide]. /// {@endtemplate} void hideToolbar() { _contextMenuController.remove(); _spellCheckToolbarController.remove(); if (_toolbar == null) { return; } _toolbar?.remove(); _toolbar?.dispose(); _toolbar = null; } /// {@template flutter.widgets.SelectionOverlay.dispose} /// Disposes this object and release resources. /// {@endtemplate} void dispose() { assert(debugMaybeDispatchDisposed(this)); hide(); _magnifierInfo.dispose(); } Widget _buildStartHandle(BuildContext context) { final Widget handle; final TextSelectionControls? selectionControls = this.selectionControls; if (selectionControls == null || (_startHandleType == TextSelectionHandleType.collapsed && _isDraggingEndHandle)) { // Hide the start handle when dragging the end handle and collapsing // the selection. handle = const SizedBox.shrink(); } else { handle = _SelectionHandleOverlay( type: _startHandleType, handleLayerLink: startHandleLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleDragStart: _handleStartHandleDragStart, onSelectionHandleDragUpdate: _handleStartHandleDragUpdate, onSelectionHandleDragEnd: _handleStartHandleDragEnd, selectionControls: selectionControls, visibility: startHandlesVisible, preferredLineHeight: _lineHeightAtStart, dragStartBehavior: dragStartBehavior, ); } return TextFieldTapRegion(child: ExcludeSemantics(child: handle)); } Widget _buildEndHandle(BuildContext context) { final Widget handle; final TextSelectionControls? selectionControls = this.selectionControls; if (selectionControls == null || (_endHandleType == TextSelectionHandleType.collapsed && _isDraggingStartHandle) || (_endHandleType == TextSelectionHandleType.collapsed && !_isDraggingStartHandle && !_isDraggingEndHandle)) { // Hide the end handle when dragging the start handle and collapsing the selection // or when the selection is collapsed and no handle is being dragged. handle = const SizedBox.shrink(); } else { handle = _SelectionHandleOverlay( type: _endHandleType, handleLayerLink: endHandleLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleDragStart: _handleEndHandleDragStart, onSelectionHandleDragUpdate: _handleEndHandleDragUpdate, onSelectionHandleDragEnd: _handleEndHandleDragEnd, selectionControls: selectionControls, visibility: endHandlesVisible, preferredLineHeight: _lineHeightAtEnd, dragStartBehavior: dragStartBehavior, ); } return TextFieldTapRegion(child: ExcludeSemantics(child: handle)); } // Build the toolbar via TextSelectionControls. Widget _buildToolbar(BuildContext context) { if (selectionControls == null) { return const SizedBox.shrink(); } assert( selectionDelegate != null, 'If not using contextMenuBuilder, must pass selectionDelegate.', ); final RenderBox renderBox = this.context.findRenderObject()! as RenderBox; final Rect editingRegion = Rect.fromPoints( renderBox.localToGlobal(Offset.zero), renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)), ); final bool isMultiline = selectionEndpoints.last.point.dy - selectionEndpoints.first.point.dy > lineHeightAtEnd / 2; // If the selected text spans more than 1 line, horizontally center the toolbar. // Derived from both iOS and Android. final double midX = isMultiline ? editingRegion.width / 2 : (selectionEndpoints.first.point.dx + selectionEndpoints.last.point.dx) / 2; final Offset midpoint = Offset( midX, // The y-coordinate won't be made use of most likely. selectionEndpoints.first.point.dy - lineHeightAtStart, ); return _SelectionToolbarWrapper( visibility: toolbarVisible, layerLink: toolbarLayerLink, offset: -editingRegion.topLeft, child: Builder( builder: (BuildContext context) { return selectionControls!.buildToolbar( context, editingRegion, lineHeightAtStart, midpoint, selectionEndpoints, selectionDelegate!, clipboardStatus, toolbarLocation, ); }, ), ); } /// {@template flutter.widgets.SelectionOverlay.updateMagnifier} /// Update the current magnifier with new selection data, so the magnifier /// can respond accordingly. /// /// If the magnifier is not shown, this still updates the magnifier position /// because the magnifier may have hidden itself and is looking for a cue to reshow /// itself. /// /// If there is no magnifier in the overlay, this does nothing. /// {@endtemplate} void updateMagnifier(MagnifierInfo magnifierInfo) { if (_magnifierController.overlayEntry == null) { return; } _magnifierInfo.value = magnifierInfo; } } // TODO(justinmc): Currently this fades in but not out on all platforms. It // should follow the correct fading behavior for the current platform, then be // made public and de-duplicated with widgets/selectable_region.dart. // https://github.com/flutter/flutter/issues/107732 // Wrap the given child in the widgets common to both contextMenuBuilder and // TextSelectionControls.buildToolbar. class _SelectionToolbarWrapper extends StatefulWidget { const _SelectionToolbarWrapper({ this.visibility, required this.layerLink, required this.offset, required this.child, }); final Widget child; final Offset offset; final LayerLink layerLink; final ValueListenable? visibility; @override State<_SelectionToolbarWrapper> createState() => _SelectionToolbarWrapperState(); } class _SelectionToolbarWrapperState extends State<_SelectionToolbarWrapper> with SingleTickerProviderStateMixin { late AnimationController _controller; Animation get _opacity => _controller.view; @override void initState() { super.initState(); _controller = AnimationController( duration: SelectionOverlay.fadeDuration, vsync: this, ); _toolbarVisibilityChanged(); widget.visibility?.addListener(_toolbarVisibilityChanged); } @override void didUpdateWidget(_SelectionToolbarWrapper oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.visibility == widget.visibility) { return; } oldWidget.visibility?.removeListener(_toolbarVisibilityChanged); _toolbarVisibilityChanged(); widget.visibility?.addListener(_toolbarVisibilityChanged); } @override void dispose() { widget.visibility?.removeListener(_toolbarVisibilityChanged); _controller.dispose(); super.dispose(); } void _toolbarVisibilityChanged() { if (widget.visibility?.value ?? true) { _controller.forward(); } else { _controller.reverse(); } } @override Widget build(BuildContext context) { return TextFieldTapRegion( child: Directionality( textDirection: Directionality.of(this.context), child: FadeTransition( opacity: _opacity, child: CompositedTransformFollower( link: widget.layerLink, showWhenUnlinked: false, offset: widget.offset, child: widget.child, ), ), ), ); } } /// This widget represents a single draggable selection handle. class _SelectionHandleOverlay extends StatefulWidget { /// Create selection overlay. const _SelectionHandleOverlay({ required this.type, required this.handleLayerLink, this.onSelectionHandleTapped, this.onSelectionHandleDragStart, this.onSelectionHandleDragUpdate, this.onSelectionHandleDragEnd, required this.selectionControls, this.visibility, required this.preferredLineHeight, this.dragStartBehavior = DragStartBehavior.start, }); final LayerLink handleLayerLink; final VoidCallback? onSelectionHandleTapped; final ValueChanged? onSelectionHandleDragStart; final ValueChanged? onSelectionHandleDragUpdate; final ValueChanged? onSelectionHandleDragEnd; final TextSelectionControls selectionControls; final ValueListenable? visibility; final double preferredLineHeight; final TextSelectionHandleType type; final DragStartBehavior dragStartBehavior; @override State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState(); } class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with SingleTickerProviderStateMixin { late AnimationController _controller; Animation get _opacity => _controller.view; @override void initState() { super.initState(); _controller = AnimationController( duration: SelectionOverlay.fadeDuration, vsync: this, ); _handleVisibilityChanged(); widget.visibility?.addListener(_handleVisibilityChanged); } void _handleVisibilityChanged() { if (widget.visibility?.value ?? true) { _controller.forward(); } else { _controller.reverse(); } } /// Returns the bounding [Rect] of the text selection handle in local /// coordinates. /// /// When interacting with a text selection handle through a touch event, the /// interactive area should be at least [kMinInteractiveDimension] square, /// which this method does not consider. Rect _getHandleRect( TextSelectionHandleType type, double preferredLineHeight, ) { final Size handleSize = widget.selectionControls.getHandleSize( preferredLineHeight, ); return Rect.fromLTRB(0.0, 0.0, handleSize.width, handleSize.height); } @override void didUpdateWidget(_SelectionHandleOverlay oldWidget) { super.didUpdateWidget(oldWidget); oldWidget.visibility?.removeListener(_handleVisibilityChanged); _handleVisibilityChanged(); widget.visibility?.addListener(_handleVisibilityChanged); } @override void dispose() { widget.visibility?.removeListener(_handleVisibilityChanged); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Rect handleRect = _getHandleRect( widget.type, widget.preferredLineHeight, ); // Make sure the GestureDetector is big enough to be easily interactive. final Rect interactiveRect = handleRect.expandToInclude( Rect.fromCircle( center: handleRect.center, radius: kMinInteractiveDimension / 2, ), ); final RelativeRect padding = RelativeRect.fromLTRB( math.max((interactiveRect.width - handleRect.width) / 2, 0), math.max((interactiveRect.height - handleRect.height) / 2, 0), math.max((interactiveRect.width - handleRect.width) / 2, 0), math.max((interactiveRect.height - handleRect.height) / 2, 0), ); final Offset handleAnchor = widget.selectionControls.getHandleAnchor( widget.type, widget.preferredLineHeight, ); // Make sure a drag is eagerly accepted. This is used on iOS to match the // behavior where a drag directly on a collapse handle will always win against // other drag gestures. final bool eagerlyAcceptDragWhenCollapsed = widget.type == TextSelectionHandleType.collapsed && defaultTargetPlatform == TargetPlatform.iOS; return CompositedTransformFollower( link: widget.handleLayerLink, // Put the handle's anchor point on the leader's anchor point. offset: -handleAnchor - Offset(padding.left, padding.top), showWhenUnlinked: false, child: FadeTransition( opacity: _opacity, child: SizedBox( width: interactiveRect.width, height: interactiveRect.height, child: Align( alignment: Alignment.topLeft, child: RawGestureDetector( behavior: HitTestBehavior.translucent, gestures: { PanGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => PanGestureRecognizer( debugOwner: this, // Mouse events select the text and do not drag the cursor. supportedDevices: { PointerDeviceKind.touch, PointerDeviceKind.stylus, PointerDeviceKind.unknown, }, ), (PanGestureRecognizer instance) { instance ..dragStartBehavior = widget.dragStartBehavior ..gestureSettings = eagerlyAcceptDragWhenCollapsed ? const DeviceGestureSettings(touchSlop: 1.0) : null ..onStart = widget.onSelectionHandleDragStart ..onUpdate = widget.onSelectionHandleDragUpdate ..onEnd = widget.onSelectionHandleDragEnd; }, ), }, child: Padding( padding: EdgeInsets.only( left: padding.left, top: padding.top, right: padding.right, bottom: padding.bottom, ), child: widget.selectionControls.buildHandle( context, widget.type, widget.preferredLineHeight, widget.onSelectionHandleTapped, ), ), ), ), ), ), ); } } ================================================ FILE: lib/common/widgets/flutter/vertical_tabs.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'dart:ui' show SemanticsRole, lerpDouble; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_instance/src/extension_instance.dart'; const double _kTabWidth = 51.0; const double _kTextAndIconTabWidth = 72.0; const EdgeInsets _kTabLabelPadding = EdgeInsets.symmetric( vertical: 7.0, horizontal: 5.0, ); /// A Material Design [VerticalTabBar] tab. /// /// If both [icon] and [text] are provided, the text is displayed below /// the icon. /// /// See also: /// /// * [VerticalTabBar], which displays a row of tabs. /// * [TabBarView], which displays a widget for the currently selected tab. /// * [TabController], which coordinates tab selection between a [VerticalTabBar] and a [TabBarView]. /// * class VerticalTab extends StatelessWidget { /// Creates a Material Design [VerticalTabBar] tab. /// /// At least one of [text], [icon], and [child] must be non-null. The [text] /// and [child] arguments must not be used at the same time. The /// [iconMargin] is only useful when [icon] and either one of [text] or /// [child] is non-null. const VerticalTab({ super.key, this.text, this.icon, this.iconMargin, this.width, this.child, }) : assert(text != null || child != null || icon != null), assert(text == null || child == null); /// The text to display as the tab's label. /// /// Must not be used in combination with [child]. final String? text; /// The widget to be used as the tab's label. /// /// Usually a [Text] widget, possibly wrapped in a [Semantics] widget. /// /// Must not be used in combination with [text]. final Widget? child; /// An icon to display as the tab's label. final Widget? icon; /// The margin added around the tab's icon. /// /// Only useful when used in combination with [icon], and either one of /// [text] or [child] is non-null. /// /// Defaults to 2 pixels of bottom margin. If [ThemeData.useMaterial3] is false, /// then defaults to 10 pixels of bottom margin. final EdgeInsetsGeometry? iconMargin; /// The height of the [VerticalTab]. /// /// If null, the height will be calculated based on the content of the [VerticalTab]. When `icon` is not /// null along with `child` or `text`, the default height is 72.0 pixels. Without an `icon`, the /// height is 46.0 pixels. /// /// {@tool snippet} /// /// The provided tab height cannot be lower than the default height. Use /// [PreferredSize] widget to adjust the overall [VerticalTabBar] height and match /// the provided tab [height]: /// /// ```dart /// bottom: const PreferredSize( /// preferredSize: Size.fromHeight(20.0), /// child: TabBar( /// tabs: [ /// Tab( /// text: 'Tab 1', /// height: 20.0, /// ), /// Tab( /// text: 'Tab 2', /// height: 20.0, /// ), /// ], /// ), /// ), /// ``` /// {@end-tool} final double? width; Widget _buildLabelText() { return child ?? Text(text!, style: const TextStyle(fontSize: 15)); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final double calculatedWidth; final Widget label; if (icon == null) { calculatedWidth = _kTabWidth; label = _buildLabelText(); } else if (text == null && child == null) { calculatedWidth = _kTabWidth; label = icon!; } else { calculatedWidth = _kTextAndIconTabWidth; final EdgeInsetsGeometry effectiveIconMargin = iconMargin ?? (Theme.of(context).useMaterial3 ? _TabsPrimaryDefaultsM3.iconMargin : _TabsDefaultsM2.iconMargin); // dom label = Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding(padding: effectiveIconMargin, child: icon), Flexible(child: _buildLabelText()), ], ); } return SizedBox( width: width ?? calculatedWidth, // dom child: Center(heightFactor: 1.0, child: label), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(StringProperty('text', text, defaultValue: null)); } } class _TabStyle extends AnimatedWidget { const _TabStyle({ required Animation animation, required this.isSelected, required this.isPrimary, required this.labelColor, required this.unselectedLabelColor, required this.labelStyle, required this.unselectedLabelStyle, required this.defaults, required this.child, }) : super(listenable: animation); final TextStyle? labelStyle; final TextStyle? unselectedLabelStyle; final bool isSelected; final bool isPrimary; final Color? labelColor; final Color? unselectedLabelColor; final TabBarThemeData defaults; final Widget child; WidgetStateColor _resolveWithLabelColor( BuildContext context, { IconThemeData? iconTheme, }) { final ThemeData themeData = Theme.of(context); final TabBarThemeData tabBarTheme = TabBarTheme.of(context); final Animation animation = listenable as Animation; // labelStyle.color (and tabBarTheme.labelStyle.color) is not considered // as it'll be a breaking change without a possible migration plan. for // details: https://github.com/flutter/flutter/pull/109541#issuecomment-1294241417 Color selectedColor = labelColor ?? tabBarTheme.labelColor ?? labelStyle?.color ?? tabBarTheme.labelStyle?.color ?? defaults.labelColor!; final Color unselectedColor; if (selectedColor is WidgetStateColor) { unselectedColor = selectedColor.resolve(const {}); selectedColor = selectedColor.resolve(const { WidgetState.selected, }); } else { // unselectedLabelColor and tabBarTheme.unselectedLabelColor are ignored // when labelColor is a WidgetStateColor. unselectedColor = unselectedLabelColor ?? tabBarTheme.unselectedLabelColor ?? unselectedLabelStyle?.color ?? tabBarTheme.unselectedLabelStyle?.color ?? iconTheme?.color ?? (themeData.useMaterial3 ? defaults.unselectedLabelColor! : selectedColor.withAlpha(0xB2)); // 70% alpha } return WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.selected)) { return Color.lerp(selectedColor, unselectedColor, animation.value)!; } return Color.lerp(unselectedColor, selectedColor, animation.value)!; }); } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final TabBarThemeData tabBarTheme = TabBarTheme.of(context); final Animation animation = listenable as Animation; final Set states = isSelected ? const {WidgetState.selected} : const {}; // To enable TextStyle.lerp(style1, style2, value), both styles must have // the same value of inherit. Force that to be inherit=true here. final TextStyle selectedStyle = defaults.labelStyle! .merge(labelStyle ?? tabBarTheme.labelStyle) .copyWith(inherit: true); final TextStyle unselectedStyle = defaults.unselectedLabelStyle! .merge( unselectedLabelStyle ?? tabBarTheme.unselectedLabelStyle ?? labelStyle, ) .copyWith(inherit: true); final TextStyle textStyle = isSelected ? TextStyle.lerp(selectedStyle, unselectedStyle, animation.value)! : TextStyle.lerp(unselectedStyle, selectedStyle, animation.value)!; final Color defaultIconColor = switch (theme.colorScheme.brightness) { Brightness.light => kDefaultIconDarkColor, Brightness.dark => kDefaultIconLightColor, }; final IconThemeData? customIconTheme = switch (IconTheme.of(context)) { final IconThemeData iconTheme when iconTheme.color != defaultIconColor => iconTheme, _ => null, }; final Color iconColor = _resolveWithLabelColor( context, iconTheme: customIconTheme, ).resolve(states); final Color labelColor = _resolveWithLabelColor(context).resolve(states); return DefaultTextStyle( style: textStyle.copyWith(color: labelColor), child: IconTheme.merge( data: IconThemeData( size: customIconTheme?.size ?? 24.0, color: iconColor, ), child: child, ), ); } } typedef _LayoutCallback = void Function( List yOffsets, TextDirection textDirection, double width, ); class _TabLabelBarRenderer extends RenderFlex { _TabLabelBarRenderer({ required super.direction, required super.mainAxisSize, required super.mainAxisAlignment, required super.crossAxisAlignment, required TextDirection super.textDirection, required super.verticalDirection, required this.onPerformLayout, }); _LayoutCallback onPerformLayout; @override void performLayout() { super.performLayout(); // yOffsets will contain childCount+1 values, giving the offsets of the // leading edge of the first tab as the first value, of the leading edge of // the each subsequent tab as each subsequent value, and of the trailing // edge of the last tab as the last value. RenderBox? child = firstChild; final List yOffsets = []; while (child != null) { final FlexParentData childParentData = child.parentData! as FlexParentData; yOffsets.add(childParentData.offset.dy); // dom assert(child.parentData == childParentData); child = childParentData.nextSibling; } yOffsets.add(size.height); onPerformLayout(yOffsets, .ltr, size.height); } } // This class and its renderer class only exist to report the widths of the tabs // upon layout. The tab widths are only used at paint time (see _IndicatorPainter) // or in response to input. class _TabLabelBar extends Flex { const _TabLabelBar({ super.children, required this.onPerformLayout, required super.mainAxisSize, }) : super( direction: Axis.vertical, // dom mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, // dom verticalDirection: VerticalDirection.down, ); final _LayoutCallback onPerformLayout; @override RenderFlex createRenderObject(BuildContext context) { return _TabLabelBarRenderer( direction: direction, mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, textDirection: .ltr, // getEffectiveTextDirection(context)!, verticalDirection: verticalDirection, onPerformLayout: onPerformLayout, ); } @override void updateRenderObject( BuildContext context, _TabLabelBarRenderer renderObject, ) { super.updateRenderObject(context, renderObject); renderObject.onPerformLayout = onPerformLayout; } } double _indexChangeProgress(TabController controller) { final double controllerValue = controller.animation!.value; final double previousIndex = controller.previousIndex.toDouble(); final double currentIndex = controller.index.toDouble(); // The controller's offset is changing because the user is dragging the // TabBarView's PageView to the left or right. if (!controller.indexIsChanging) { return clampDouble((currentIndex - controllerValue).abs(), 0.0, 1.0); } // The TabController animation's value is changing from previousIndex to currentIndex. return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs(); } class _DividerPainter extends CustomPainter { _DividerPainter({required this.dividerColor, required this.dividerWidth}); final Color dividerColor; final double dividerWidth; @override void paint(Canvas canvas, Size size) { if (dividerWidth <= 0.0) { return; } final Paint paint = Paint() ..color = dividerColor ..strokeWidth = dividerWidth; // dom final dx = size.width - (paint.strokeWidth / 2); canvas.drawLine(Offset(dx, 0), Offset(dx, size.height), paint); } @override bool shouldRepaint(_DividerPainter oldDelegate) { return oldDelegate.dividerColor != dividerColor || oldDelegate.dividerWidth != dividerWidth; } } class _IndicatorPainter extends CustomPainter { _IndicatorPainter({ required this.controller, required this.indicator, required this.indicatorSize, required this.tabKeys, required _IndicatorPainter? old, required this.indicatorPadding, required this.labelPadding, this.dividerColor, this.dividerWidth, required this.showDivider, this.devicePixelRatio, required this.indicatorAnimation, required this.textDirection, }) : super(repaint: controller.animation) { assert(debugMaybeDispatchCreated('material', '_IndicatorPainter', this)); if (old != null) { saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); } } final TabController controller; final Decoration indicator; final TabBarIndicatorSize indicatorSize; final EdgeInsetsGeometry indicatorPadding; final List tabKeys; final EdgeInsetsGeometry labelPadding; final Color? dividerColor; final double? dividerWidth; final bool showDivider; final double? devicePixelRatio; final TabIndicatorAnimation indicatorAnimation; final TextDirection textDirection; // _currentTabOffsets and _currentTextDirection are set each time TabBar // layout is completed. These values can be null when TabBar contains no // tabs, since there are nothing to lay out. List? _currentTabOffsets; TextDirection? _currentTextDirection; Rect? _currentRect; BoxPainter? _painter; bool _needsPaint = false; void markNeedsPaint() { _needsPaint = true; } void dispose() { assert(debugMaybeDispatchDisposed(this)); _painter?.dispose(); } void saveTabOffsets(List? tabOffsets, TextDirection? textDirection) { _currentTabOffsets = tabOffsets; _currentTextDirection = textDirection; } // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab. int get maxTabIndex => _currentTabOffsets!.length - 2; double centerOf(int tabIndex) { assert(_currentTabOffsets != null); assert(_currentTabOffsets!.isNotEmpty); assert(tabIndex >= 0); assert(tabIndex <= maxTabIndex); return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) / 2.0; } Rect indicatorRect(Size tabBarSize, int tabIndex) { assert(_currentTabOffsets != null); assert(_currentTextDirection != null); assert(_currentTabOffsets!.isNotEmpty); assert(tabIndex >= 0); assert(tabIndex <= maxTabIndex); double tabLeft, tabRight; (tabLeft, tabRight) = switch (_currentTextDirection!) { TextDirection.rtl => ( _currentTabOffsets![tabIndex + 1], _currentTabOffsets![tabIndex], ), TextDirection.ltr => ( _currentTabOffsets![tabIndex], _currentTabOffsets![tabIndex + 1], ), }; if (indicatorSize == TabBarIndicatorSize.label) { final double tabWidth = tabKeys[tabIndex].currentContext!.size!.height; // dom final EdgeInsets insets = labelPadding.resolve(_currentTextDirection); final double delta = ((tabRight - tabLeft) - (tabWidth + insets.vertical)) / 2.0; // dom tabLeft += delta + insets.top; // dom tabRight = tabLeft + tabWidth; } final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection); // dom final Rect rect = Rect.fromLTWH( 0, tabLeft, tabBarSize.width, tabRight - tabLeft, ); if (!(rect.size >= insets.collapsedSize)) { throw FlutterError( 'indicatorPadding insets should be less than Tab Size\n' 'Rect Size : ${rect.size}, Insets: $insets', ); } return insets.deflateRect(rect); } @override void paint(Canvas canvas, Size size) { _needsPaint = false; _painter ??= indicator.createBoxPainter(markNeedsPaint); final double value = controller.animation!.value; _currentRect = switch (indicatorAnimation) { TabIndicatorAnimation.linear => _applyLinearEffect( size: size, value: value, ), TabIndicatorAnimation.elastic => _applyElasticEffect( size: size, value: value, ), }; assert(_currentRect != null); final ImageConfiguration configuration = ImageConfiguration( size: _currentRect!.size, textDirection: _currentTextDirection, devicePixelRatio: devicePixelRatio, ); if (showDivider && dividerWidth! > 0) { final Paint dividerPaint = Paint() ..color = dividerColor! ..strokeWidth = dividerWidth!; final dx = size.width - (dividerPaint.strokeWidth / 2); final Offset dividerP1 = Offset(dx, 0); final Offset dividerP2 = Offset(dx, size.height); // dom canvas.drawLine(dividerP1, dividerP2, dividerPaint); } _painter!.paint(canvas, _currentRect!.topLeft, configuration); } /// Applies the linear effect to the indicator. Rect? _applyLinearEffect({required Size size, required double value}) { final double index = controller.index.toDouble(); final bool ltr = index > value; final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex); final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex); final Rect fromRect = indicatorRect(size, from); final Rect toRect = indicatorRect(size, to); return Rect.lerp(fromRect, toRect, (value - from).abs()); } // Ease out sine (decelerating). double decelerateInterpolation(double fraction) { return math.sin((fraction * math.pi) / 2.0); } // Ease in sine (accelerating). double accelerateInterpolation(double fraction) { return 1.0 - math.cos((fraction * math.pi) / 2.0); } /// Applies the elastic effect to the indicator. // dom Rect? _applyElasticEffect({required Size size, required double value}) { final double index = controller.index.toDouble(); double progressLeft = (index - value).abs(); final int to = progressLeft == 0.0 || !controller.indexIsChanging ? switch (textDirection) { TextDirection.ltr => value.ceil(), TextDirection.rtl => value.floor(), }.clamp(0, maxTabIndex) : controller.index; final int from = progressLeft == 0.0 || !controller.indexIsChanging ? switch (textDirection) { TextDirection.ltr => (to - 1), TextDirection.rtl => (to + 1), }.clamp(0, maxTabIndex) : controller.previousIndex; final Rect toRect = indicatorRect(size, to); final Rect fromRect = indicatorRect(size, from); final Rect rect = Rect.lerp(fromRect, toRect, (value - from).abs())!; // If the tab animation is completed, there is no need to stretch the indicator // This only works for the tab change animation via tab index, not when // dragging a [TabBarView], but it's still ok, to avoid unnecessary calculations. if (controller.animation!.isCompleted) { return rect; } final double tabChangeProgress; if (controller.indexIsChanging) { final int tabsDelta = (controller.index - controller.previousIndex).abs(); if (tabsDelta != 0) { progressLeft /= tabsDelta; } tabChangeProgress = 1 - clampDouble(progressLeft, 0.0, 1.0); } else { tabChangeProgress = (index - value).abs(); } // If the animation has finished, there is no need to apply the stretch effect. if (tabChangeProgress == 1.0) { return rect; } final double topFraction; final double bottomFraction; final bool isMovingRight = switch (textDirection) { TextDirection.ltr => controller.indexIsChanging ? index > value : value > index, TextDirection.rtl => controller.indexIsChanging ? value > index : index > value, }; if (isMovingRight) { topFraction = accelerateInterpolation(tabChangeProgress); bottomFraction = decelerateInterpolation(tabChangeProgress); } else { topFraction = decelerateInterpolation(tabChangeProgress); bottomFraction = accelerateInterpolation(tabChangeProgress); } final double lerpRectTop; final double lerpRectBottom; // The controller.indexIsChanging is true when the Tab is pressed, instead of swipe to change tabs. // If the tab is pressed then only lerp between fromRect and toRect. if (controller.indexIsChanging) { lerpRectTop = lerpDouble(fromRect.top, toRect.top, topFraction)!; lerpRectBottom = lerpDouble( fromRect.bottom, toRect.bottom, bottomFraction, )!; } else { // Switch the Rect left and right lerp order based on swipe direction. lerpRectTop = switch (isMovingRight) { true => lerpDouble(fromRect.top, toRect.top, topFraction)!, false => lerpDouble(toRect.top, fromRect.top, topFraction)!, }; lerpRectBottom = switch (isMovingRight) { true => lerpDouble(fromRect.bottom, toRect.bottom, bottomFraction)!, false => lerpDouble(toRect.bottom, fromRect.bottom, bottomFraction)!, }; } return Rect.fromLTRB(rect.left, lerpRectTop, rect.right, lerpRectBottom); } @override bool shouldRepaint(_IndicatorPainter old) { return _needsPaint || controller != old.controller || indicator != old.indicator || tabKeys.length != old.tabKeys.length || (!listEquals(_currentTabOffsets, old._currentTabOffsets)) || _currentTextDirection != old._currentTextDirection; } } class _ChangeAnimation extends Animation with AnimationWithParentMixin { _ChangeAnimation(this.controller); final TabController controller; @override Animation get parent => controller.animation!; @override void removeStatusListener(AnimationStatusListener listener) { if (controller.animation != null) { super.removeStatusListener(listener); } } @override void removeListener(VoidCallback listener) { if (controller.animation != null) { super.removeListener(listener); } } @override double get value => _indexChangeProgress(controller); } class _DragAnimation extends Animation with AnimationWithParentMixin { _DragAnimation(this.controller, this.index); final TabController controller; final int index; @override Animation get parent => controller.animation!; @override void removeStatusListener(AnimationStatusListener listener) { if (controller.animation != null) { super.removeStatusListener(listener); } } @override void removeListener(VoidCallback listener) { if (controller.animation != null) { super.removeListener(listener); } } @override double get value { assert(!controller.indexIsChanging); final double controllerMaxValue = (controller.length - 1).toDouble(); final double controllerValue = clampDouble( controller.animation!.value, 0.0, controllerMaxValue, ); return clampDouble((controllerValue - index.toDouble()).abs(), 0.0, 1.0); } } // This class, and TabBarScrollController, only exist to handle the case // where a scrollable TabBar has a non-zero initialIndex. In that case we can // only compute the scroll position's initial scroll offset (the "correct" // pixels value) after the TabBar viewport width and scroll limits are known. class _TabBarScrollPosition extends ScrollPositionWithSingleContext { _TabBarScrollPosition({ required super.physics, required super.context, required super.oldPosition, required this.tabBar, }) : super(initialPixels: null); final _VerticalTabBarState tabBar; bool _viewportDimensionWasNonZero = false; // The scroll position should be adjusted at least once. bool _needsPixelsCorrection = true; @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { bool result = true; if (!_viewportDimensionWasNonZero) { _viewportDimensionWasNonZero = viewportDimension != 0.0; } // If the viewport never had a non-zero dimension, we just want to jump // to the initial scroll position to avoid strange scrolling effects in // release mode: the viewport temporarily may have a dimension of zero // before the actual dimension is calculated. In that scenario, setting // the actual dimension would cause a strange scroll effect without this // guard because the super call below would start a ballistic scroll activity. if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) { _needsPixelsCorrection = false; correctPixels( tabBar._initialScrollOffset( viewportDimension, minScrollExtent, maxScrollExtent, ), ); result = false; } return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result; } void markNeedsPixelsCorrection() { _needsPixelsCorrection = true; } } // This class, and TabBarScrollPosition, only exist to handle the case // where a scrollable TabBar has a non-zero initialIndex. class _TabBarScrollController extends ScrollController { _TabBarScrollController(this.tabBar); final _VerticalTabBarState tabBar; @override ScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition, ) { return _TabBarScrollPosition( physics: physics, context: context, oldPosition: oldPosition, tabBar: tabBar, ); } } /// A Material Design primary tab bar. /// /// Primary tabs are placed at the top of the content pane under a top app bar. /// They display the main content destinations. /// /// Typically created as the [AppBar.bottom] part of an [AppBar] and in /// conjunction with a [TabBarView]. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40} /// /// If a [TabController] is not provided, then a [DefaultTabController] ancestor /// must be provided instead. The tab controller's [TabController.length] must /// equal the length of the [tabs] list and the length of the /// [TabBarView.children] list. /// /// Requires one of its ancestors to be a [Material] widget. /// /// Uses values from [TabBarThemeData] if it is set in the current context. /// /// {@tool dartpad} /// This sample shows the implementation of [VerticalTabBar] and [TabBarView] using a [DefaultTabController]. /// Each [VerticalTab] corresponds to a child of the [TabBarView] in the order they are written. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// [VerticalTabBar] can also be implemented by using a [TabController] which provides more options /// to control the behavior of the [VerticalTabBar] and [TabBarView]. This can be used instead of /// a [DefaultTabController], demonstrated below. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This sample showcases nested Material 3 [VerticalTabBar]s. It consists of a primary /// [VerticalTabBar] with nested a secondary [VerticalTabBar]. The primary [VerticalTabBar] uses a /// [DefaultTabController] while the secondary [VerticalTabBar] uses a [TabController]. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.2.dart ** /// {@end-tool} /// /// See also: /// /// * [TabBar.secondary], for a secondary tab bar. /// * [TabBarView], which displays page views that correspond to each tab. /// * [TabController], which coordinates tab selection between a [VerticalTabBar] and a [TabBarView]. /// * https://m3.material.io/components/tabs/overview, the Material 3 /// tab bar specification. class VerticalTabBar extends StatefulWidget { /// Creates a Material Design primary tab bar. /// /// The length of the [tabs] argument must match the [controller]'s /// [TabController.length]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. /// /// The [indicatorWeight] parameter defaults to 2. /// /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero]. /// /// If [indicator] is not null or provided from [TabBarTheme], /// then [indicatorWeight] and [indicatorColor] are ignored. const VerticalTabBar({ super.key, required this.tabs, this.controller, this.isScrollable = false, this.padding, this.indicatorColor, this.automaticIndicatorColorAdjustment = true, this.indicatorWeight = 2.0, this.indicatorPadding = EdgeInsets.zero, this.indicator, this.indicatorSize, this.dividerColor, this.dividerWidth, this.labelColor, this.labelStyle, this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, this.dragStartBehavior = DragStartBehavior.start, this.overlayColor, this.mouseCursor, this.enableFeedback, this.onTap, this.onHover, this.onFocusChange, this.physics, this.splashFactory, this.splashBorderRadius, this.tabAlignment, this.textScaler, this.indicatorAnimation, }) : _isPrimary = true, assert(indicator != null || (indicatorWeight > 0.0)); /// Creates a Material Design secondary tab bar. /// /// Secondary tabs are used within a content area to further separate related /// content and establish hierarchy. /// /// {@tool dartpad} /// This sample showcases nested Material 3 [VerticalTabBar]s. It consists of a primary /// [VerticalTabBar] with nested a secondary [VerticalTabBar]. The primary [VerticalTabBar] uses a /// [DefaultTabController] while the secondary [VerticalTabBar] uses a [TabController]. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.2.dart ** /// {@end-tool} /// /// See also: /// /// * [VerticalTabBar], for a primary tab bar. /// * [TabBarView], which displays page views that correspond to each tab. /// * [TabController], which coordinates tab selection between a [VerticalTabBar] and a [TabBarView]. /// * https://m3.material.io/components/tabs/overview, the Material 3 /// tab bar specification. const VerticalTabBar.secondary({ super.key, required this.tabs, this.controller, this.isScrollable = false, this.padding, this.indicatorColor, this.automaticIndicatorColorAdjustment = true, this.indicatorWeight = 2.0, this.indicatorPadding = EdgeInsets.zero, this.indicator, this.indicatorSize, this.dividerColor, this.dividerWidth, this.labelColor, this.labelStyle, this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, this.dragStartBehavior = DragStartBehavior.start, this.overlayColor, this.mouseCursor, this.enableFeedback, this.onTap, this.onHover, this.onFocusChange, this.physics, this.splashFactory, this.splashBorderRadius, this.tabAlignment, this.textScaler, this.indicatorAnimation, }) : _isPrimary = false, assert(indicator != null || (indicatorWeight > 0.0)); /// Typically a list of two or more [VerticalTab] widgets. /// /// The length of this list must match the [controller]'s [TabController.length] /// and the length of the [TabBarView.children] list. final List tabs; /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController? controller; /// Whether this tab bar can be scrolled horizontally. /// /// If [isScrollable] is true, then each tab is as wide as needed for its label /// and the entire [VerticalTabBar] is scrollable. Otherwise each tab gets an equal /// share of the available space. final bool isScrollable; /// The amount of space by which to inset the tab bar. /// /// When [isScrollable] is false, this will yield the same result as if [VerticalTabBar] was wrapped /// in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset, /// allowing the padding to scroll with the tab bar, rather than enclosing it. final EdgeInsetsGeometry? padding; /// The color of the line that appears below the selected tab. /// /// If this parameter is null, then the value of the Theme's indicatorColor /// property is used. /// /// If [indicator] is specified or provided from [TabBarThemeData], /// this property is ignored. final Color? indicatorColor; /// The thickness of the line that appears below the selected tab. /// /// The value of this parameter must be greater than zero. /// /// If [ThemeData.useMaterial3] is true and [VerticalTabBar] is used to create a /// primary tab bar, the default value is 3.0. If the provided value is less /// than 3.0, the default value is used. /// /// If [ThemeData.useMaterial3] is true and [TabBar.secondary] is used to /// create a secondary tab bar, the default value is 2.0. /// /// If [ThemeData.useMaterial3] is false, the default value is 2.0. /// /// If [indicator] is specified or provided from [TabBarThemeData], /// this property is ignored. final double indicatorWeight; /// The padding for the indicator. /// /// The default value of this property is [EdgeInsets.zero]. /// /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align /// the indicator with the tab's text for [VerticalTab] widgets and all but the /// shortest [VerticalTab.text] values. final EdgeInsetsGeometry indicatorPadding; /// Defines the appearance of the selected tab indicator. /// /// If [indicator] is specified or provided from [TabBarThemeData], /// the [indicatorColor] and [indicatorWeight] properties are ignored. /// /// The default, underline-style, selected tab indicator can be defined with /// [UnderlineTabIndicator]. /// /// The indicator's size is based on the tab's bounds. If [indicatorSize] /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space /// occupied by the tab in the tab bar. If [indicatorSize] is /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as /// the tab widget itself. /// /// See also: /// /// * [splashBorderRadius], which defines the clipping radius of the splash /// and is generally used with [BoxDecoration.borderRadius]. final Decoration? indicator; /// Whether this tab bar should automatically adjust the [indicatorColor]. /// /// The default value of this property is true. /// /// If [automaticIndicatorColorAdjustment] is true, /// then the [indicatorColor] will be automatically adjusted to [Colors.white] /// when the [indicatorColor] is same as [Material.color] of the [Material] /// parent widget. final bool automaticIndicatorColorAdjustment; /// Defines how the selected tab indicator's size is computed. /// /// The size of the selected tab indicator is defined relative to the /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab] /// (the default) or relative to the bounds of the tab's widget if /// [indicatorSize] is [TabBarIndicatorSize.label]. /// /// The selected tab's location appearance can be refined further with /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and /// [indicator] properties. final TabBarIndicatorSize? indicatorSize; /// The color of the divider. /// /// If the [dividerColor] is [Colors.transparent], then the divider will not be drawn. /// /// If null and [ThemeData.useMaterial3] is false, [TabBarThemeData.dividerColor] /// color is used. If that is null and [ThemeData.useMaterial3] is true, /// [ColorScheme.outlineVariant] will be used, otherwise divider will not be drawn. final Color? dividerColor; /// The height of the divider. /// /// If the [dividerWidth] is zero or negative, then the divider will not be drawn. /// /// If null and [ThemeData.useMaterial3] is true, [TabBarThemeData.dividerHeight] is used. /// If that is also null and [ThemeData.useMaterial3] is true, 1.0 will be used. /// Otherwise divider will not be drawn. final double? dividerWidth; /// The color of selected tab labels. /// /// If null, then [TabBarThemeData.labelColor] is used. If that is also null and /// [ThemeData.useMaterial3] is true, [ColorScheme.primary] will be used, /// otherwise the color of the [ThemeData.primaryTextTheme]'s /// [TextTheme.bodyLarge] text color is used. /// /// If [labelColor] (or, if null, [TabBarThemeData.labelColor]) is a /// [WidgetStateColor], then the effective tab color will depend on the /// [WidgetState.selected] state, i.e. if the [VerticalTab] is selected or not, /// ignoring [unselectedLabelColor] even if it's non-null. /// /// When this color or the [TabBarThemeData.labelColor] is specified, it overrides /// the [TextStyle.color] specified for the [labelStyle] or the /// [TabBarThemeData.labelStyle]. /// /// See also: /// /// * [unselectedLabelColor], for color of unselected tab labels. final Color? labelColor; /// The color of unselected tab labels. /// /// If [labelColor] (or, if null, [TabBarThemeData.labelColor]) is a /// [WidgetStateColor], then the unselected tabs are rendered with /// that [WidgetStateColor]'s resolved color for unselected state, even if /// [unselectedLabelColor] is non-null. /// /// If null, then [TabBarThemeData.unselectedLabelColor] is used. If that is also /// null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant] /// will be used, otherwise unselected tab labels are rendered with /// [labelColor] at 70% opacity. /// /// When this color or the [TabBarThemeData.unselectedLabelColor] is specified, it /// overrides the [TextStyle.color] specified for the [unselectedLabelStyle] /// or the [TabBarThemeData.unselectedLabelStyle]. /// /// See also: /// /// * [labelColor], for color of selected tab labels. final Color? unselectedLabelColor; /// The text style of the selected tab labels. /// /// The color specified in [labelStyle] and [TabBarThemeData.labelStyle] is used /// to style the label when [labelColor] or [TabBarThemeData.labelColor] are not /// specified. /// /// If [unselectedLabelStyle] is null, then this text style will be used for /// both selected and unselected label styles. /// /// If this property is null, then [TabBarThemeData.labelStyle] will be used. /// /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall] /// will be used, otherwise the text style of the [ThemeData.primaryTextTheme]'s /// [TextTheme.bodyLarge] definition is used. final TextStyle? labelStyle; /// The text style of the unselected tab labels. /// /// The color specified in [unselectedLabelStyle] and [TabBarThemeData.unselectedLabelStyle] /// is used to style the label when [unselectedLabelColor] or [TabBarThemeData.unselectedLabelColor] /// are not specified. /// /// If this property is null, then [TabBarThemeData.unselectedLabelStyle] will be used. /// /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall] /// will be used, otherwise then the [labelStyle] value is used. If [labelStyle] is null, /// the text style of the [ThemeData.primaryTextTheme]'s [TextTheme.bodyLarge] /// definition is used. final TextStyle? unselectedLabelStyle; /// The padding added to each of the tab labels. /// /// If there are few tabs with both icon and text and few /// tabs with only icon or text, this padding is vertically /// adjusted to provide uniform padding to all tabs. /// /// If this property is null, then [kTabLabelPadding] is used. final EdgeInsetsGeometry? labelPadding; /// Defines the ink response focus, hover, and splash colors. /// /// If non-null, it is resolved against one of [WidgetState.focused], /// [WidgetState.hovered], and [WidgetState.pressed]. /// /// [WidgetState.pressed] triggers a ripple (an ink splash), per /// the current Material Design spec. /// /// If the overlay color is null or resolves to null, then if [ThemeData.useMaterial3] is /// false, the default values for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor], /// and [InkResponse.highlightColor] will be used instead. If [ThemeData.useMaterial3] /// if true, the default values are: /// * selected: /// * pressed - ThemeData.colorScheme.primary(0.1) /// * hovered - ThemeData.colorScheme.primary(0.08) /// * focused - ThemeData.colorScheme.primary(0.1) /// * pressed - ThemeData.colorScheme.primary(0.1) /// * hovered - ThemeData.colorScheme.onSurface(0.08) /// * focused - ThemeData.colorScheme.onSurface(0.1) final WidgetStateProperty? overlayColor; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@template flutter.material.tabs.mouseCursor} /// The cursor for a mouse pointer when it enters or is hovering over the /// individual tab widgets. /// /// If [mouseCursor] is a [WidgetStateMouseCursor], /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// /// * [WidgetState.selected]. /// {@endtemplate} /// /// If null, then the value of [TabBarThemeData.mouseCursor] is used. If /// that is also null, then [WidgetStateMouseCursor.clickable] is used. final MouseCursor? mouseCursor; /// Whether detected gestures should provide acoustic and/or haptic feedback. /// /// For example, on Android a tap will produce a clicking sound and a long-press /// will produce a short vibration, when feedback is enabled. /// /// Defaults to true. final bool? enableFeedback; /// An optional callback that's called when the [VerticalTabBar] is tapped. /// /// The callback is applied to the index of the tab where the tap occurred. /// /// This callback has no effect on the default handling of taps. It's for /// applications that want to do a little extra work when a tab is tapped, /// even if the tap doesn't change the TabController's index. TabBar [onTap] /// callbacks should not make changes to the TabController since that would /// interfere with the default tap handler. final ValueChanged? onTap; /// An optional callback that's called when a [VerticalTab]'s hover state in the /// [VerticalTabBar] changes. /// /// Called when a pointer enters or exits the ink response area of the [VerticalTab]. /// /// The value passed to the callback is true if a pointer has entered the /// [VerticalTab] at `index` and false if a pointer has exited. /// /// When hover is moved from one tab directly to another, this will be called /// twice. First to represent hover exiting the initial tab, and then second /// for the pointer entering hover over the next tab. /// /// {@tool dartpad} /// This sample shows how to customize a [VerticalTab] in response to hovering over a /// [VerticalTabBar]. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.onHover.dart ** /// {@end-tool} final TabValueChanged? onHover; /// An optional callback that's called when a [VerticalTab]'s focus state in the /// [VerticalTabBar] changes. /// /// Called when the node for the [VerticalTab] at `index` gains or loses focus. /// /// The value passed to the callback is true if the node has gained focus for /// the [VerticalTab] at `index` and false if focus has been lost. /// /// When focus is moved from one tab directly to another, this will be called /// twice. First to represent focus being lost by the initially focused tab, /// and then second for the next tab gaining focus. /// /// {@tool dartpad} /// This sample shows how to customize a [VerticalTab] based on focus traversal in /// enclosing [VerticalTabBar]. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.onFocusChange.dart ** /// {@end-tool} final TabValueChanged? onFocusChange; /// How the [VerticalTabBar]'s scroll view should respond to user input. /// /// For example, determines how the scroll view continues to animate after the /// user stops dragging the scroll view. /// /// Defaults to matching platform conventions. final ScrollPhysics? physics; /// Creates the tab bar's [InkWell] splash factory, which defines /// the appearance of "ink" splashes that occur in response to taps. /// /// Use [NoSplash.splashFactory] to defeat ink splash rendering. For example /// to defeat both the splash and the hover/pressed overlay, but not the /// keyboard focused overlay: /// /// ```dart /// TabBar( /// splashFactory: NoSplash.splashFactory, /// overlayColor: WidgetStateProperty.resolveWith( /// (Set states) { /// return states.contains(WidgetState.focused) ? null : Colors.transparent; /// }, /// ), /// tabs: const [ /// // ... /// ], /// ) /// ``` final InteractiveInkFeatureFactory? splashFactory; /// Defines the clipping radius of splashes that extend outside the bounds of the tab. /// /// This can be useful to match the [BoxDecoration.borderRadius] provided as [indicator]. /// /// ```dart /// TabBar( /// indicator: BoxDecoration( /// borderRadius: BorderRadius.circular(40), /// ), /// splashBorderRadius: BorderRadius.circular(40), /// tabs: const [ /// // ... /// ], /// ) /// ``` /// /// If this property is null, it is interpreted as [BorderRadius.zero]. final BorderRadius? splashBorderRadius; /// Specifies the horizontal alignment of the tabs within a [VerticalTabBar]. /// /// If [VerticalTabBar.isScrollable] is false, only [TabAlignment.fill] and /// [TabAlignment.center] are supported. Otherwise an exception is thrown. /// /// If [VerticalTabBar.isScrollable] is true, only [TabAlignment.start], [TabAlignment.startOffset], /// and [TabAlignment.center] are supported. Otherwise an exception is thrown. /// /// If this is null, then the value of [TabBarThemeData.tabAlignment] is used. /// /// If [TabBarThemeData.tabAlignment] is null and [ThemeData.useMaterial3] is true, /// then [TabAlignment.startOffset] is used if [isScrollable] is true, /// otherwise [TabAlignment.fill] is used. /// /// If [TabBarThemeData.tabAlignment] is null and [ThemeData.useMaterial3] is false, /// then [TabAlignment.center] is used if [isScrollable] is true, /// otherwise [TabAlignment.fill] is used. final TabAlignment? tabAlignment; /// Specifies the text scaling behavior for the [VerticalTab] label. /// /// If this is null, then the value of [TabBarThemeData.textScaler] is used. If that is /// also null, then the text scaling behavior is determined by the [MediaQueryData.textScaler] /// from the ambient [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. /// /// See also: /// * [TextScaler], which is used to scale text based on the device's text scale factor. final TextScaler? textScaler; /// Specifies the animation behavior of the tab indicator. /// /// If this is null, then the value of [TabBarThemeData.indicatorAnimation] is used. /// If that is also null, then the tab indicator will animate linearly if the /// [indicatorSize] is [TabBarIndicatorSize.tab], otherwise it will animate /// with an elastic effect if the [indicatorSize] is [TabBarIndicatorSize.label]. /// /// {@tool dartpad} /// This sample shows how to customize the animation behavior of the tab indicator /// by using the [indicatorAnimation] property. /// /// ** See code in examples/api/lib/material/tabs/tab_bar.indicator_animation.0.dart ** /// {@end-tool} /// /// See also: /// /// * [TabIndicatorAnimation], which specifies the animation behavior of the tab indicator. final TabIndicatorAnimation? indicatorAnimation; /// Returns whether the [VerticalTabBar] contains a tab with both text and icon. /// /// [VerticalTabBar] uses this to give uniform padding to all tabs in cases where /// there are some tabs with both text and icon and some which contain only /// text or icon. bool get tabHasTextAndIcon { for (final Widget item in tabs) { if (item is PreferredSizeWidget) { if (item.preferredSize.height == _kTextAndIconTabWidth) { return true; } } } return false; } /// Whether this tab bar is a primary tab bar. /// /// Otherwise, it is a secondary tab bar. final bool _isPrimary; @override State createState() => _VerticalTabBarState(); } class _VerticalTabBarState extends State { ScrollController? _scrollController; TabController? _controller; _IndicatorPainter? _indicatorPainter; int? _currentIndex; // late double _tabStripWidth; late List _tabKeys; EdgeInsetsGeometry _labelPadding = EdgeInsets.zero; bool _debugHasScheduledValidTabsCountCheck = false; @override void initState() { super.initState(); // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find // the width of tab widget i. See _IndicatorPainter.indicatorRect(). _tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList(); // _labelPaddings = List.filled( // widget.tabs.length, // EdgeInsets.zero, // growable: true, // ); } TabBarThemeData get _defaults { if (Theme.of(context).useMaterial3) { return widget._isPrimary ? _TabsPrimaryDefaultsM3(context, widget.isScrollable) : _TabsSecondaryDefaultsM3(context, widget.isScrollable); } else { return _TabsDefaultsM2(context, widget.isScrollable); } } Decoration _getIndicator(TabBarIndicatorSize indicatorSize) { final ThemeData theme = Theme.of(context); final TabBarThemeData tabBarTheme = TabBarTheme.of(context); if (widget.indicator != null) { return widget.indicator!; } if (tabBarTheme.indicator != null) { return tabBarTheme.indicator!; } Color color = widget.indicatorColor ?? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!; // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab bar is on a // Material that isn't the primaryColor. In that case, if the indicator // color ends up matching the material's color, then this overrides it. // When that happens, automatic transitions of the theme will likely look // ugly as the indicator color suddenly snaps to white at one end, but it's // not clear how to avoid that any further. // // The material's color might be null (if it's a transparency). In that case // there's no good way for us to find out what the color is so we don't. // // TODO(xu-baolin): Remove automatic adjustment to white color indicator // with a better long-term solution. // https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917 if (widget.automaticIndicatorColorAdjustment && color.toARGB32() == Material.maybeOf(context)?.color?.toARGB32()) { color = Colors.white; } final double effectiveIndicatorWeight = theme.useMaterial3 ? math.max(widget.indicatorWeight, switch (widget._isPrimary) { true => _TabsPrimaryDefaultsM3.indicatorWeight(indicatorSize), false => _TabsSecondaryDefaultsM3.indicatorWeight, }) : widget.indicatorWeight; // Only Material 3 primary TabBar with label indicatorSize should be rounded. final bool primaryWithLabelIndicator = switch (indicatorSize) { TabBarIndicatorSize.label => widget._isPrimary, TabBarIndicatorSize.tab => false, }; final BorderRadius? effectiveBorderRadius = theme.useMaterial3 && primaryWithLabelIndicator // dom ? BorderRadius.only( topRight: Radius.circular(effectiveIndicatorWeight), bottomRight: Radius.circular(effectiveIndicatorWeight), ) : null; return VerticalUnderlineTabIndicator( borderRadius: effectiveBorderRadius, borderSide: BorderSide( // TODO(tahatesser): Make sure this value matches Material 3 Tabs spec // when `preferredSize`and `indicatorWeight` are updated to support Material 3 // https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30, // https://github.com/flutter/flutter/issues/116136 width: effectiveIndicatorWeight, color: color, ), ); } // If the TabBar is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController? newController = widget.controller ?? DefaultTabController.maybeOf(context); assert(() { if (newController == null) { throw FlutterError( 'No TabController for ${widget.runtimeType}.\n' 'When creating a ${widget.runtimeType}, you must either provide an explicit ' 'TabController using the "controller" property, or you must ensure that there ' 'is a DefaultTabController above the ${widget.runtimeType}.\n' 'In this case, there was neither an explicit controller nor a default controller.', ); } return true; }()); if (newController == _controller) { return; } if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); _controller!.removeListener(_handleTabControllerTick); } _controller = newController; if (_controller != null) { _controller!.animation!.addListener(_handleTabControllerAnimationTick); _controller!.addListener(_handleTabControllerTick); _currentIndex = _controller!.index; } } void _initIndicatorPainter() { final ThemeData theme = Theme.of(context); final TabBarThemeData tabBarTheme = TabBarTheme.of(context); final TabBarIndicatorSize indicatorSize = widget.indicatorSize ?? tabBarTheme.indicatorSize ?? _defaults.indicatorSize!; final _IndicatorPainter? oldPainter = _indicatorPainter; final TabIndicatorAnimation defaultTabIndicatorAnimation = switch (indicatorSize) { TabBarIndicatorSize.label => TabIndicatorAnimation.elastic, TabBarIndicatorSize.tab => TabIndicatorAnimation.linear, }; _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter( controller: _controller!, indicator: _getIndicator(indicatorSize), indicatorSize: indicatorSize, indicatorPadding: widget.indicatorPadding, tabKeys: _tabKeys, // Passing old painter so that the constructor can copy some values from it. old: oldPainter, labelPadding: _labelPadding, dividerColor: widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor, dividerWidth: widget.dividerWidth ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight, showDivider: theme.useMaterial3 && !widget.isScrollable, devicePixelRatio: MediaQuery.devicePixelRatioOf(context), indicatorAnimation: widget.indicatorAnimation ?? tabBarTheme.indicatorAnimation ?? defaultTabIndicatorAnimation, textDirection: .ltr, ); oldPainter?.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); _updateTabController(); _initIndicatorPainter(); } @override void didUpdateWidget(VerticalTabBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _updateTabController(); _initIndicatorPainter(); // Adjust scroll position. if (_scrollController != null && _scrollController!.hasClients) { final ScrollPosition position = _scrollController!.position; if (position is _TabBarScrollPosition) { position.markNeedsPixelsCorrection(); } } } else if (widget.indicatorColor != oldWidget.indicatorColor || widget.indicatorWeight != oldWidget.indicatorWeight || widget.indicatorSize != oldWidget.indicatorSize || widget.indicatorPadding != oldWidget.indicatorPadding || widget.indicator != oldWidget.indicator || widget.dividerColor != oldWidget.dividerColor || widget.dividerWidth != oldWidget.dividerWidth || widget.indicatorAnimation != oldWidget.indicatorAnimation) { _initIndicatorPainter(); } if (widget.tabs.length > _tabKeys.length) { final int delta = widget.tabs.length - _tabKeys.length; _tabKeys.addAll(List.generate(delta, (int n) => GlobalKey())); // _labelPaddings.addAll( // List.filled(delta, EdgeInsets.zero), // ); } else if (widget.tabs.length < _tabKeys.length) { _tabKeys.removeRange(widget.tabs.length, _tabKeys.length); // _labelPaddings.removeRange(widget.tabs.length, _tabKeys.length); } } @override void dispose() { _indicatorPainter!.dispose(); if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); _controller!.removeListener(_handleTabControllerTick); } _controller = null; _scrollController?.dispose(); // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } int get maxTabIndex => _indicatorPainter!.maxTabIndex; final _mainCtr = Get.find(); double _tabScrollOffset( int index, double viewportWidth, double minExtent, double maxExtent, ) { if (!widget.isScrollable) { return 0.0; } double tabCenter = _indicatorPainter!.centerOf(index); // double paddingStart; // switch (Directionality.of(context)) { // case TextDirection.rtl: // paddingStart = widget.padding?.resolve(TextDirection.rtl).right ?? 0; // tabCenter = _tabStripWidth - tabCenter; // case TextDirection.ltr: // paddingStart = widget.padding?.resolve(TextDirection.ltr).left ?? 0; // } // dom final double paddingTop = widget.padding?.resolve(TextDirection.ltr).top ?? 0; return clampDouble( tabCenter + paddingTop - viewportWidth / 2.0 + (_mainCtr.useBottomNav && switch (_mainCtr.barHideType) { .instant => _mainCtr.showBottomBar?.value ?? true, .sync => (_mainCtr.barOffset?.value ?? 0) == 0, } ? 80.0 : 0.0), minExtent, maxExtent, ); } double _tabCenteredScrollOffset(int index) { final ScrollPosition position = _scrollController!.position; return _tabScrollOffset( index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent, ); } double _initialScrollOffset( double viewportWidth, double minExtent, double maxExtent, ) { return _tabScrollOffset( _currentIndex!, viewportWidth, minExtent, maxExtent, ); } void _scrollToCurrentIndex() { final double offset = _tabCenteredScrollOffset(_currentIndex!); _scrollController!.animateTo( offset, duration: kTabScrollDuration, curve: Curves.ease, ); } void _scrollToControllerValue() { final double? leadingPosition = _currentIndex! > 0 ? _tabCenteredScrollOffset(_currentIndex! - 1) : null; final double middlePosition = _tabCenteredScrollOffset(_currentIndex!); final double? trailingPosition = _currentIndex! < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex! + 1) : null; final double index = _controller!.index.toDouble(); final double value = _controller!.animation!.value; final double offset = switch (value - index) { -1.0 => leadingPosition ?? middlePosition, 1.0 => trailingPosition ?? middlePosition, 0 => middlePosition, < 0 => leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value)!, _ => trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index)!, }; _scrollController!.jumpTo(offset); } void _handleTabControllerAnimationTick() { assert(mounted); if (!_controller!.indexIsChanging && widget.isScrollable) { // Sync the TabBar's scroll position with the TabBarView's PageView. _currentIndex = _controller!.index; _scrollToControllerValue(); } } void _handleTabControllerTick() { if (_controller!.index != _currentIndex) { _currentIndex = _controller!.index; if (widget.isScrollable) { _scrollToCurrentIndex(); } } setState(() { // Rebuild the tabs after a (potentially animated) index change // has completed. }); } // Called each time layout completes. void _saveTabOffsets( List tabOffsets, TextDirection textDirection, double width, ) { // _tabStripWidth = width; _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection); } void _handleTap(int index) { assert(index >= 0 && index < widget.tabs.length); _controller!.animateTo(index); widget.onTap?.call(index); } Widget _buildStyledTab( Widget child, bool isSelected, Animation animation, TabBarThemeData defaults, ) { return _TabStyle( animation: animation, isSelected: isSelected, isPrimary: widget._isPrimary, labelColor: widget.labelColor, unselectedLabelColor: widget.unselectedLabelColor, labelStyle: widget.labelStyle, unselectedLabelStyle: widget.unselectedLabelStyle, defaults: defaults, child: child, ); } bool _debugScheduleCheckHasValidTabsCount() { if (_debugHasScheduledValidTabsCountCheck) { return true; } WidgetsBinding.instance.addPostFrameCallback((Duration duration) { _debugHasScheduledValidTabsCountCheck = false; if (!mounted) { return; } assert(() { if (_controller!.length != widget.tabs.length) { throw FlutterError( "Controller's length property (${_controller!.length}) does not match the " "number of tabs (${widget.tabs.length}) present in TabBar's tabs property.", ); } return true; }()); }, debugLabel: 'TabBar.tabsCountCheck'); _debugHasScheduledValidTabsCountCheck = true; return true; } bool _debugTabAlignmentIsValid(TabAlignment tabAlignment) { assert(() { if (widget.isScrollable && tabAlignment == TabAlignment.fill) { throw FlutterError( '$tabAlignment is only valid for non-scrollable tab bars.', ); } if (!widget.isScrollable && (tabAlignment == TabAlignment.start || tabAlignment == TabAlignment.startOffset)) { throw FlutterError( '$tabAlignment is only valid for scrollable tab bars.', ); } return true; }()); return true; } @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); assert(_debugScheduleCheckHasValidTabsCount()); final ThemeData theme = Theme.of(context); final TabBarThemeData tabBarTheme = TabBarTheme.of(context); final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!; assert(_debugTabAlignmentIsValid(effectiveTabAlignment)); // final MaterialLocalizations localizations = MaterialLocalizations.of( // context, // ); if (_controller!.length == 0) { return LimitedBox( maxWidth: 0.0, child: SizedBox( width: double.infinity, height: _kTabWidth + widget.indicatorWeight, ), ); } final List wrappedTabs = List.generate(widget.tabs.length, ( int index, ) { EdgeInsetsGeometry padding = widget.labelPadding ?? tabBarTheme.labelPadding ?? _kTabLabelPadding; // const double verticalAdjustment = // (_kTextAndIconTabWidth - _kTabWidth) / 2.0; // // final Widget tab = widget.tabs[index]; // if (tab is PreferredSizeWidget && // tab.preferredSize.height == _kTabWidth && // widget.tabHasTextAndIcon) { // padding = padding.add( // const EdgeInsets.symmetric(vertical: verticalAdjustment), // ); // } _labelPadding = padding; return Center( widthFactor: 1.0, child: Padding( padding: _labelPadding, child: KeyedSubtree(key: _tabKeys[index], child: widget.tabs[index]), ), ); }); // If the controller was provided by DefaultTabController and we're part // of a Hero (typically the AppBar), then we will not be able to find the // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213. if (_controller != null) { final int previousIndex = _controller!.previousIndex; if (_controller!.indexIsChanging) { // The user tapped on a tab, the tab controller's animation is running. assert(_currentIndex != previousIndex); final Animation animation = _ChangeAnimation(_controller!); wrappedTabs[_currentIndex!] = _buildStyledTab( wrappedTabs[_currentIndex!], true, animation, _defaults, ); wrappedTabs[previousIndex] = _buildStyledTab( wrappedTabs[previousIndex], false, animation, _defaults, ); } else { // The user is dragging the TabBarView's PageView left or right. final int tabIndex = _currentIndex!; final Animation centerAnimation = _DragAnimation( _controller!, tabIndex, ); wrappedTabs[tabIndex] = _buildStyledTab( wrappedTabs[tabIndex], true, centerAnimation, _defaults, ); if (_currentIndex! > 0) { final int tabIndex = _currentIndex! - 1; final Animation previousAnimation = ReverseAnimation( _DragAnimation(_controller!, tabIndex), ); wrappedTabs[tabIndex] = _buildStyledTab( wrappedTabs[tabIndex], false, previousAnimation, _defaults, ); } if (_currentIndex! < widget.tabs.length - 1) { final int tabIndex = _currentIndex! + 1; final Animation nextAnimation = ReverseAnimation( _DragAnimation(_controller!, tabIndex), ); wrappedTabs[tabIndex] = _buildStyledTab( wrappedTabs[tabIndex], false, nextAnimation, _defaults, ); } } } // Add the tap handler to each tab. If the tab bar is not scrollable, // then give all of the tabs equal flexibility so that they each occupy // the same share of the tab bar's overall width. final int tabCount = widget.tabs.length; for (int index = 0; index < tabCount; index += 1) { final Set selectedState = { if (index == _currentIndex) WidgetState.selected, }; final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs( widget.mouseCursor, selectedState, ) ?? tabBarTheme.mouseCursor?.resolve(selectedState) ?? WidgetStateMouseCursor.clickable.resolve(selectedState); final WidgetStateProperty defaultOverlay = WidgetStateProperty.resolveWith(( Set states, ) { final Set effectiveStates = selectedState.toSet() ..addAll(states); return _defaults.overlayColor?.resolve(effectiveStates); }); wrappedTabs[index] = InkWell( mouseCursor: effectiveMouseCursor, onTap: () { _handleTap(index); }, onHover: (bool value) { widget.onHover?.call(value, index); }, onFocusChange: (bool value) { widget.onFocusChange?.call(value, index); }, enableFeedback: widget.enableFeedback ?? true, overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay, splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? _defaults.splashFactory, borderRadius: widget.splashBorderRadius ?? tabBarTheme.splashBorderRadius ?? _defaults.splashBorderRadius, child: Padding( padding: EdgeInsets.only(left: widget.indicatorWeight), child: wrappedTabs[index], ), ); wrappedTabs[index] = MergeSemantics(child: wrappedTabs[index]); if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) { wrappedTabs[index] = Expanded(child: wrappedTabs[index]); } } Widget tabBar = Semantics( role: SemanticsRole.tabBar, container: true, explicitChildNodes: true, child: CustomPaint( painter: _indicatorPainter, child: _TabStyle( animation: kAlwaysDismissedAnimation, isSelected: false, isPrimary: widget._isPrimary, labelColor: widget.labelColor, unselectedLabelColor: widget.unselectedLabelColor, labelStyle: widget.labelStyle, unselectedLabelStyle: widget.unselectedLabelStyle, defaults: _defaults, child: _TabLabelBar( onPerformLayout: _saveTabOffsets, mainAxisSize: effectiveTabAlignment == TabAlignment.fill ? MainAxisSize.max : MainAxisSize.min, children: wrappedTabs, ), ), ), ); if (widget.isScrollable) { _scrollController ??= _TabBarScrollController(this); tabBar = ScrollConfiguration( // The scrolling tabs should not show an overscroll indicator. behavior: ScrollConfiguration.of(context).copyWith(overscroll: false), child: SingleChildScrollView( dragStartBehavior: widget.dragStartBehavior, scrollDirection: Axis.vertical, // dom controller: _scrollController, padding: widget.padding, physics: widget.physics, child: tabBar, ), ); if (theme.useMaterial3) { final AlignmentGeometry effectiveAlignment = switch (effectiveTabAlignment) { TabAlignment.center => Alignment.center, TabAlignment.start || TabAlignment.startOffset || TabAlignment.fill => AlignmentDirectional.topCenter, // dom }; final Color dividerColor = widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor!; final double dividerWidth = widget.dividerWidth ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight!; tabBar = Align( widthFactor: 1.0, heightFactor: null, // dom // heightFactor: dividerWidth > 0 ? null : 1.0, alignment: effectiveAlignment, child: tabBar, ); if (dividerColor != Colors.transparent && dividerWidth > 0) { tabBar = CustomPaint( painter: _DividerPainter( dividerColor: dividerColor, dividerWidth: dividerWidth, ), child: tabBar, ); } } } else if (widget.padding != null) { tabBar = Padding(padding: widget.padding!, child: tabBar); } return MediaQuery( data: MediaQuery.of( context, ).copyWith(textScaler: widget.textScaler ?? tabBarTheme.textScaler), child: tabBar, ); } } // Hand coded defaults based on Material Design 2. class _TabsDefaultsM2 extends TabBarThemeData { _TabsDefaultsM2(this.context, this.isScrollable) : super(indicatorSize: TabBarIndicatorSize.tab); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; late final bool isDark = Theme.brightnessOf(context) == Brightness.dark; late final Color primaryColor = isDark ? Colors.grey[900]! : Colors.blue; final bool isScrollable; @override Color? get indicatorColor => _colors.secondary == primaryColor ? Colors.white : _colors.secondary; @override Color? get labelColor => Theme.of(context).primaryTextTheme.bodyLarge!.color!; @override TextStyle? get labelStyle => Theme.of(context).primaryTextTheme.bodyLarge; @override TextStyle? get unselectedLabelStyle => Theme.of(context).primaryTextTheme.bodyLarge; @override InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; static const EdgeInsetsGeometry iconMargin = EdgeInsets.only(bottom: 10); } // BEGIN GENERATED TOKEN PROPERTIES - Tabs // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // dart format off class _TabsPrimaryDefaultsM3 extends TabBarThemeData { _TabsPrimaryDefaultsM3(this.context, this.isScrollable) : super(indicatorSize: TabBarIndicatorSize.label); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; late final TextTheme _textTheme = Theme.of(context).textTheme; final bool isScrollable; // This value comes from Divider widget defaults. Token db deprecated 'primary-navigation-tab.divider.color' token. @override Color? get dividerColor => _colors.outlineVariant; // This value comes from Divider widget defaults. Token db deprecated 'primary-navigation-tab.divider.height' token. @override double? get dividerHeight => 1.0; @override Color? get indicatorColor => _colors.primary; @override Color? get labelColor => _colors.primary; @override TextStyle? get labelStyle => _textTheme.titleSmall; @override Color? get unselectedLabelColor => _colors.onSurfaceVariant; @override TextStyle? get unselectedLabelStyle => _textTheme.titleSmall; @override WidgetStateProperty get overlayColor { return WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.pressed)) { return _colors.primary.withValues(alpha:0.1); } if (states.contains(WidgetState.hovered)) { return _colors.primary.withValues(alpha:0.08); } if (states.contains(WidgetState.focused)) { return _colors.primary.withValues(alpha:0.1); } return null; } if (states.contains(WidgetState.pressed)) { return _colors.primary.withValues(alpha:0.1); } if (states.contains(WidgetState.hovered)) { return _colors.onSurface.withValues(alpha:0.08); } if (states.contains(WidgetState.focused)) { return _colors.onSurface.withValues(alpha:0.1); } return null; }); } @override InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; static double indicatorWeight(TabBarIndicatorSize indicatorSize) { return switch (indicatorSize) { TabBarIndicatorSize.label => 3.0, TabBarIndicatorSize.tab => 2.0, }; } // TODO(davidmartos96): This value doesn't currently exist in // https://m3.material.io/components/tabs/specs // Update this when the token is available. static const EdgeInsetsGeometry iconMargin = EdgeInsets.only(bottom: 2); } class _TabsSecondaryDefaultsM3 extends TabBarThemeData { _TabsSecondaryDefaultsM3(this.context, this.isScrollable) : super(indicatorSize: TabBarIndicatorSize.tab); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; late final TextTheme _textTheme = Theme.of(context).textTheme; final bool isScrollable; // This value comes from Divider widget defaults. Token db deprecated 'secondary-navigation-tab.divider.color' token. @override Color? get dividerColor => _colors.outlineVariant; // This value comes from Divider widget defaults. Token db deprecated 'secondary-navigation-tab.divider.height' token. @override double? get dividerHeight => 1.0; @override Color? get indicatorColor => _colors.primary; @override Color? get labelColor => _colors.onSurface; @override TextStyle? get labelStyle => _textTheme.titleSmall; @override Color? get unselectedLabelColor => _colors.onSurfaceVariant; @override TextStyle? get unselectedLabelStyle => _textTheme.titleSmall; @override WidgetStateProperty get overlayColor { return WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.pressed)) { return _colors.onSurface.withValues(alpha:0.1); } if (states.contains(WidgetState.hovered)) { return _colors.onSurface.withValues(alpha: 0.08); } if (states.contains(WidgetState.focused)) { return _colors.onSurface.withValues(alpha:0.1); } return null; } if (states.contains(WidgetState.pressed)) { return _colors.onSurface.withValues(alpha:0.1); } if (states.contains(WidgetState.hovered)) { return _colors.onSurface.withValues(alpha:0.08); } if (states.contains(WidgetState.focused)) { return _colors.onSurface.withValues(alpha:0.1); } return null; }); } @override InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; static double indicatorWeight = 2.0; } // dart format on // END GENERATED TOKEN PROPERTIES - Tabs /// Used with [TabBar.indicator] to draw a horizontal line below the /// selected tab. /// /// The selected tab underline is inset from the tab's boundary by [insets]. /// The [borderSide] defines the line's color and weight. /// /// The [TabBar.indicatorSize] property can be used to define the indicator's /// bounds in terms of its (centered) widget with [TabBarIndicatorSize.label], /// or the entire tab with [TabBarIndicatorSize.tab]. class VerticalUnderlineTabIndicator extends Decoration { /// Create an underline style selected tab indicator. const VerticalUnderlineTabIndicator({ this.borderRadius, this.borderSide = const BorderSide(width: 2.0, color: Colors.white), this.insets = EdgeInsets.zero, }); /// The radius of the indicator's corners. /// /// If this value is non-null, rounded rectangular tab indicator is /// drawn, otherwise rectangular tab indicator is drawn. final BorderRadius? borderRadius; /// The color and weight of the horizontal line drawn below the selected tab. final BorderSide borderSide; /// Locates the selected tab's underline relative to the tab's boundary. /// /// The [TabBar.indicatorSize] property can be used to define the tab /// indicator's bounds in terms of its (centered) tab widget with /// [TabBarIndicatorSize.label], or the entire tab with /// [TabBarIndicatorSize.tab]. final EdgeInsetsGeometry insets; @override Decoration? lerpFrom(Decoration? a, double t) { if (a is VerticalUnderlineTabIndicator) { return VerticalUnderlineTabIndicator( borderSide: BorderSide.lerp(a.borderSide, borderSide, t), insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!, ); } return super.lerpFrom(a, t); } @override Decoration? lerpTo(Decoration? b, double t) { if (b is VerticalUnderlineTabIndicator) { return VerticalUnderlineTabIndicator( borderSide: BorderSide.lerp(borderSide, b.borderSide, t), insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!, ); } return super.lerpTo(b, t); } @override BoxPainter createBoxPainter([VoidCallback? onChanged]) { return _VerticalUnderlinePainter(this, borderRadius, onChanged); } Rect _indicatorRectFor(Rect rect, TextDirection textDirection) { final Rect indicator = insets.resolve(textDirection).deflateRect(rect); // dom return Rect.fromLTWH( indicator.left, indicator.top, borderSide.width, indicator.height, ); } @override Path getClipPath(Rect rect, TextDirection textDirection) { if (borderRadius != null) { return Path()..addRRect( borderRadius!.toRRect(_indicatorRectFor(rect, textDirection)), ); } return Path()..addRect(_indicatorRectFor(rect, textDirection)); } } class _VerticalUnderlinePainter extends BoxPainter { _VerticalUnderlinePainter( this.decoration, this.borderRadius, super.onChanged, ); final VerticalUnderlineTabIndicator decoration; final BorderRadius? borderRadius; @override void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { assert(configuration.size != null); final Rect rect = offset & configuration.size!; final TextDirection textDirection = configuration.textDirection!; final Paint paint; if (borderRadius != null) { paint = Paint()..color = decoration.borderSide.color; final Rect indicator = decoration._indicatorRectFor(rect, textDirection); final RRect rrect = RRect.fromRectAndCorners( indicator, topLeft: borderRadius!.topLeft, topRight: borderRadius!.topRight, bottomRight: borderRadius!.bottomRight, bottomLeft: borderRadius!.bottomLeft, ); canvas.drawRRect(rrect, paint); } else { paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.square; final Rect indicator = decoration ._indicatorRectFor(rect, textDirection) .deflate(decoration.borderSide.width / 2.0); // dom canvas.drawLine(indicator.topLeft, indicator.bottomLeft, paint); } } } ================================================ FILE: lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart ================================================ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/gestures.dart'; class CustomHorizontalDragGestureRecognizer extends HorizontalDragGestureRecognizer { CustomHorizontalDragGestureRecognizer({ super.debugOwner, super.supportedDevices, super.allowedButtonsFilter, }); Offset? _initialPosition; Offset? get initialPosition => _initialPosition; @override DeviceGestureSettings get gestureSettings => _gestureSettings; final _gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH); @override void addAllowedPointer(PointerDownEvent event) { super.addAllowedPointer(event); _initialPosition = event.position; } @override bool hasSufficientGlobalDistanceToAccept( PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop, ) { return _computeHitSlop( globalDistanceMoved.abs(), gestureSettings, pointerDeviceKind, _initialPosition, lastPosition.global, ); } } double touchSlopH = Pref.touchSlopH; bool _computeHitSlop( double globalDistanceMoved, DeviceGestureSettings? settings, PointerDeviceKind kind, Offset? initialPosition, Offset lastPosition, ) { switch (kind) { case PointerDeviceKind.mouse: return globalDistanceMoved > kPrecisePointerHitSlop; case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: case PointerDeviceKind.unknown: case PointerDeviceKind.touch: return globalDistanceMoved > touchSlopH && _calc(initialPosition!, lastPosition); case PointerDeviceKind.trackpad: return globalDistanceMoved > (settings?.touchSlop ?? kTouchSlop); } } bool _calc(Offset initialPosition, Offset lastPosition) { final offset = lastPosition - initialPosition; return offset.dx.abs() > offset.dy.abs() * 3; } ================================================ FILE: lib/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart ================================================ import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:flutter/gestures.dart'; mixin ImageGestureRecognizerMixin on GestureRecognizer { int? _pointer; @override void addPointer(PointerDownEvent event) { if (_pointer == event.pointer) { return; } _pointer = event.pointer; super.addPointer(event); } } typedef IsBoundaryAllowed = bool Function(Offset? initialPosition, OffsetPair lastPosition); class ImageHorizontalDragGestureRecognizer extends CustomHorizontalDragGestureRecognizer with ImageGestureRecognizerMixin { ImageHorizontalDragGestureRecognizer({ super.debugOwner, super.supportedDevices, super.allowedButtonsFilter, }); IsBoundaryAllowed? isBoundaryAllowed; @override bool hasSufficientGlobalDistanceToAccept( PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop, ) { return super.hasSufficientGlobalDistanceToAccept( pointerDeviceKind, deviceTouchSlop, ) && (isBoundaryAllowed?.call(initialPosition, lastPosition) ?? true); } @override void dispose() { isBoundaryAllowed = null; super.dispose(); } } ================================================ FILE: lib/common/widgets/gesture/immediate_tap_gesture_recognizer.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer { ImmediateTapGestureRecognizer({ super.debugOwner, super.supportedDevices, super.allowedButtonsFilter = _defaultButtonAcceptBehavior, this.onTapDown, this.onTapUp, this.onTapCancel, this.onTap, }); static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton; GestureTapDownCallback? onTapDown; GestureTapUpCallback? onTapUp; GestureTapCancelCallback? onTapCancel; GestureTapCallback? onTap; PointerUpEvent? _up; int? _activePointer; bool _sentTapDown = false; bool _wonArena = false; Offset? _initialPosition; @override bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => false; @override bool isPointerAllowed(PointerDownEvent event) => _activePointer == null && super.isPointerAllowed(event); @override void addAllowedPointer(PointerDownEvent event) { super.addAllowedPointer(event); _reset(event.pointer); _handleTapDown(event); _initialPosition = event.position; } @override void handleEvent(PointerEvent event) { if (event.pointer != _activePointer) { resolvePointer(event.pointer, GestureDisposition.rejected); stopTrackingPointer(event.pointer); return; } if (event is PointerMoveEvent) { _handlePointerMove(event); } else if (event is PointerUpEvent) { _up = event; _handlePointerUp(event); } else if (event is PointerCancelEvent) { resolve(GestureDisposition.rejected); } stopTrackingIfPointerNoLongerDown(event); } void _handleTapDown(PointerDownEvent event) { if (_sentTapDown) return; _sentTapDown = true; if (onTapDown != null) { final details = TapDownDetails( globalPosition: event.position, localPosition: event.localPosition, kind: event.kind, ); invokeCallback('onTapDown', () => onTapDown!(details)); } } void _handlePointerMove(PointerMoveEvent event) { if ((event.position - _initialPosition!).distanceSquared > 4.0) { resolve(GestureDisposition.rejected); stopTrackingPointer(event.pointer); } } void _handlePointerUp(PointerUpEvent event) { if (_wonArena) { _handleTapUp(event); } } void _handleTapUp(PointerUpEvent event) { if (onTapUp != null) { final details = TapUpDetails( globalPosition: event.position, localPosition: event.localPosition, kind: event.kind, ); invokeCallback('onTapUp', () => onTapUp!(details)); } if (onTap != null) { invokeCallback('onTap', onTap!); } _reset(); } void _cancelGesture(String reason) { if (_sentTapDown && onTapCancel != null) { invokeCallback('onTapCancel: $reason', onTapCancel!); } _reset(); } void _reset([int? pointer]) { _activePointer = pointer; _up = null; _sentTapDown = false; _wonArena = false; } @override void acceptGesture(int pointer) { super.acceptGesture(pointer); if (pointer == _activePointer) { _wonArena = true; if (_up != null) { _handleTapUp(_up!); } } } @override void rejectGesture(int pointer) { super.rejectGesture(pointer); if (pointer == _activePointer) { _cancelGesture('gesture rejected by arena'); stopTrackingPointer(pointer); } } @override void didStopTrackingLastPointer(int pointer) { _initialPosition = null; } @override String get debugDescription => 'immediate tap'; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(IntProperty('activePointer', _activePointer)) ..add( FlagProperty( 'sentTapDown', value: _sentTapDown, ifTrue: 'has sentTapDown', ), ) ..add(FlagProperty('wonArena', value: _wonArena, ifTrue: 'wonArena')) ..add( DiagnosticsProperty( 'pointerUpEvent', _up, defaultValue: null, ), ); } } ================================================ FILE: lib/common/widgets/gesture/mouse_interactive_viewer.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io' show Platform; import 'dart:math' as math; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart' show Quad, Vector3; class MouseInteractiveViewer extends StatefulWidget { const MouseInteractiveViewer({ super.key, this.clipBehavior = Clip.hardEdge, this.panAxis = PanAxis.free, this.boundaryMargin = EdgeInsets.zero, this.constrained = true, this.maxScale = 2.5, this.minScale = 0.8, this.interactionEndFrictionCoefficient = _kDrag, this.pointerSignalFallback, this.onPointerPanZoomUpdate, this.onPointerPanZoomEnd, this.onPointerDown, this.onInteractionEnd, this.onInteractionStart, this.onInteractionUpdate, this.panEnabled = true, this.scaleEnabled = true, this.scaleFactor = kDefaultMouseScrollToScaleFactor, this.transformationController, this.alignment, this.trackpadScrollCausesScale = false, required this.childKey, required this.child, required this.onTranslate, }) : assert(minScale > 0), assert(interactionEndFrictionCoefficient > 0), assert(maxScale > 0), assert(maxScale >= minScale); final Alignment? alignment; final Clip clipBehavior; final PanAxis panAxis; final EdgeInsets boundaryMargin; final Widget child; final bool constrained; final bool panEnabled; final bool scaleEnabled; final bool trackpadScrollCausesScale; final double scaleFactor; final double maxScale; final double minScale; final double interactionEndFrictionCoefficient; final PointerSignalEventListener? pointerSignalFallback; final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate; final PointerPanZoomEndEventListener? onPointerPanZoomEnd; final PointerDownEventListener? onPointerDown; final GestureScaleEndCallback? onInteractionEnd; final GestureScaleStartCallback? onInteractionStart; final GestureScaleUpdateCallback? onInteractionUpdate; final TransformationController? transformationController; final GlobalKey childKey; final VoidCallback onTranslate; static const double _kDrag = 0.0000135; @override State createState() => _MouseInteractiveViewerState(); } class _MouseInteractiveViewerState extends State with TickerProviderStateMixin { late TransformationController _transformer = widget.transformationController ?? TransformationController(); final GlobalKey _parentKey = GlobalKey(); Animation? _animation; Animation? _scaleAnimation; late Offset _scaleAnimationFocalPoint; late AnimationController _controller; late AnimationController _scaleController; Axis? _currentAxis; Offset? _referenceFocalPoint; double? _scaleStart; double? _rotationStart = 0.0; double _currentRotation = 0.0; _GestureType? _gestureType; static final gestureSettings = DeviceGestureSettings( touchSlop: Platform.isIOS ? 9 : 4, ); late final ScaleGestureRecognizer _scaleGestureRecognizer; final bool _rotateEnabled = false; Rect get _boundaryRect { assert(widget.childKey.currentContext != null); final RenderBox childRenderBox = widget.childKey.currentContext!.findRenderObject()! as RenderBox; final Size childSize = childRenderBox.size; final Rect boundaryRect = widget.boundaryMargin.inflateRect( Offset.zero & childSize, ); assert( !boundaryRect.isEmpty, "InteractiveViewer's child must have nonzero dimensions.", ); assert( boundaryRect.isFinite || (boundaryRect.left.isInfinite && boundaryRect.top.isInfinite && boundaryRect.right.isInfinite && boundaryRect.bottom.isInfinite), 'boundaryRect must either be infinite in all directions or finite in all directions.', ); return boundaryRect; } Rect get _viewport { assert(_parentKey.currentContext != null); final RenderBox parentRenderBox = _parentKey.currentContext!.findRenderObject()! as RenderBox; return Offset.zero & parentRenderBox.size; } Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { if (translation == Offset.zero) { return matrix.clone(); } final Offset alignedTranslation; if (_currentAxis != null) { alignedTranslation = switch (widget.panAxis) { PanAxis.horizontal => _alignAxis(translation, Axis.horizontal), PanAxis.vertical => _alignAxis(translation, Axis.vertical), PanAxis.aligned => _alignAxis(translation, _currentAxis!), PanAxis.free => translation, }; } else { alignedTranslation = translation; } final Matrix4 nextMatrix = matrix.clone() ..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1); final Quad nextViewport = _transformViewport(nextMatrix, _viewport); if (_boundaryRect.isInfinite) { return nextMatrix; } final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation( _boundaryRect, _currentRotation, ); final Offset offendingDistance = _exceedsBy( boundariesAabbQuad, nextViewport, ); if (offendingDistance == Offset.zero) { return nextMatrix; } final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix); final double currentScale = matrix.getMaxScaleOnAxis(); final Offset correctedTotalTranslation = Offset( nextTotalTranslation.dx - offendingDistance.dx * currentScale, nextTotalTranslation.dy - offendingDistance.dy * currentScale, ); final Matrix4 correctedMatrix = matrix.clone() ..setTranslation( Vector3( correctedTotalTranslation.dx, correctedTotalTranslation.dy, 0.0, ), ); final Quad correctedViewport = _transformViewport( correctedMatrix, _viewport, ); final Offset offendingCorrectedDistance = _exceedsBy( boundariesAabbQuad, correctedViewport, ); if (offendingCorrectedDistance == Offset.zero) { return correctedMatrix; } if (offendingCorrectedDistance.dx != 0.0 && offendingCorrectedDistance.dy != 0.0) { return matrix.clone(); } final Offset unidirectionalCorrectedTotalTranslation = Offset( offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, ); return matrix.clone()..setTranslation( Vector3( unidirectionalCorrectedTotalTranslation.dx, unidirectionalCorrectedTotalTranslation.dy, 0.0, ), ); } Matrix4 _matrixScale(Matrix4 matrix, double scale) { if (scale == 1.0) { return matrix.clone(); } assert(scale != 0.0); final double currentScale = _transformer.value.getMaxScaleOnAxis(); final double totalScale = math.max( currentScale * scale, math.max( _viewport.width / _boundaryRect.width, _viewport.height / _boundaryRect.height, ), ); final double clampedTotalScale = clampDouble( totalScale, widget.minScale, widget.maxScale, ); final double clampedScale = clampedTotalScale / currentScale; return matrix.clone() ..scaleByDouble(clampedScale, clampedScale, clampedScale, 1); } Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) { if (rotation == 0) { return matrix.clone(); } final Offset focalPointScene = _transformer.toScene(focalPoint); return matrix.clone() ..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1) ..rotateZ(-rotation) ..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1); } bool _gestureIsSupported(_GestureType? gestureType) { return switch (gestureType) { _GestureType.rotate => _rotateEnabled, _GestureType.scale => widget.scaleEnabled, _GestureType.pan || null => widget.panEnabled, }; } _GestureType _getGestureType(ScaleUpdateDetails details) { final double scale = !widget.scaleEnabled ? 1.0 : details.scale; final double rotation = !_rotateEnabled ? 0.0 : details.rotation; if ((scale - 1).abs() > rotation.abs()) { return _GestureType.scale; } else if (rotation != 0.0) { return _GestureType.rotate; } else { return _GestureType.pan; } } // Handle the start of a gesture. All of pan, scale, and rotate are handled // with GestureDetector's scale gesture. void _onScaleStart(ScaleStartDetails details) { widget.onInteractionStart?.call(details); if (_controller.isAnimating) { _controller ..stop() ..reset(); _animation?.removeListener(_handleInertiaAnimation); _animation = null; } if (_scaleController.isAnimating) { _scaleController ..stop() ..reset(); _scaleAnimation?.removeListener(_handleScaleAnimation); _scaleAnimation = null; } _gestureType = null; _currentAxis = null; _scaleStart = _transformer.value.getMaxScaleOnAxis(); _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); _rotationStart = _currentRotation; } // Handle an update to an ongoing gesture. All of pan, scale, and rotate are // handled with GestureDetector's scale gesture. void _onScaleUpdate(ScaleUpdateDetails details) { final double scale = _transformer.value.getMaxScaleOnAxis(); _scaleAnimationFocalPoint = details.localFocalPoint; final Offset focalPointScene = _transformer.toScene( details.localFocalPoint, ); if (_gestureType == _GestureType.pan) { // When a gesture first starts, it sometimes has no change in scale and // rotation despite being a two-finger gesture. Here the gesture is // allowed to be reinterpreted as its correct type after originally // being marked as a pan. _gestureType = _getGestureType(details); } else { _gestureType ??= _getGestureType(details); } if (!_gestureIsSupported(_gestureType)) { widget.onInteractionUpdate?.call(details); return; } switch (_gestureType!) { case _GestureType.scale: assert(_scaleStart != null); // details.scale gives us the amount to change the scale as of the // start of this gesture, so calculate the amount to scale as of the // previous call to _onScaleUpdate. final double desiredScale = _scaleStart! * details.scale; final double scaleChange = desiredScale / scale; _transformer.value = _matrixScale(_transformer.value, scaleChange); // While scaling, translate such that the user's two fingers stay on // the same places in the scene. That means that the focal point of // the scale should be on the same place in the scene before and after // the scale. final Offset focalPointSceneScaled = _transformer.toScene( details.localFocalPoint, ); _transformer.value = _matrixTranslate( _transformer.value, focalPointSceneScaled - _referenceFocalPoint!, ); // details.localFocalPoint should now be at the same location as the // original _referenceFocalPoint point. If it's not, that's because // the translate came in contact with a boundary. In that case, update // _referenceFocalPoint so subsequent updates happen in relation to // the new effective focal point. final Offset focalPointSceneCheck = _transformer.toScene( details.localFocalPoint, ); if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { _referenceFocalPoint = focalPointSceneCheck; } case _GestureType.rotate: if (details.rotation == 0.0) { widget.onInteractionUpdate?.call(details); return; } final double desiredRotation = _rotationStart! + details.rotation; _transformer.value = _matrixRotate( _transformer.value, _currentRotation - desiredRotation, details.localFocalPoint, ); _currentRotation = desiredRotation; case _GestureType.pan: assert(_referenceFocalPoint != null); // details may have a change in scale here when scaleEnabled is false. // In an effort to keep the behavior similar whether or not scaleEnabled // is true, these gestures are thrown away. if (details.scale != 1.0) { widget.onInteractionUpdate?.call(details); return; } _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); // Translate so that the same point in the scene is underneath the // focal point before and after the movement. final Offset translationChange = focalPointScene - _referenceFocalPoint!; _transformer.value = _matrixTranslate( _transformer.value, translationChange, ); _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); } widget.onInteractionUpdate?.call(details); } // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate // are handled with GestureDetector's scale gesture. void _onScaleEnd(ScaleEndDetails details) { widget.onInteractionEnd?.call(details); _scaleStart = null; _rotationStart = null; _referenceFocalPoint = null; _animation?.removeListener(_handleInertiaAnimation); _scaleAnimation?.removeListener(_handleScaleAnimation); _controller.reset(); _scaleController.reset(); if (!_gestureIsSupported(_gestureType)) { _currentAxis = null; return; } switch (_gestureType) { case _GestureType.pan: if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { _currentAxis = null; return; } final Vector3 translationVector = _transformer.value.getTranslation(); final Offset translation = Offset( translationVector.x, translationVector.y, ); final FrictionSimulation frictionSimulationX = FrictionSimulation( widget.interactionEndFrictionCoefficient, translation.dx, details.velocity.pixelsPerSecond.dx, ); final FrictionSimulation frictionSimulationY = FrictionSimulation( widget.interactionEndFrictionCoefficient, translation.dy, details.velocity.pixelsPerSecond.dy, ); final double tFinal = _getFinalTime( details.velocity.pixelsPerSecond.distance, widget.interactionEndFrictionCoefficient, ); _animation = _controller.drive( Tween( begin: translation, end: Offset( frictionSimulationX.finalX, frictionSimulationY.finalX, ), ).chain(CurveTween(curve: Curves.decelerate)), )..addListener(_handleInertiaAnimation); _controller ..duration = Duration(milliseconds: (tFinal * 1000).round()) ..forward(); case _GestureType.scale: if (details.scaleVelocity.abs() < 0.1) { _currentAxis = null; return; } final double scale = _transformer.value.getMaxScaleOnAxis(); final FrictionSimulation frictionSimulation = FrictionSimulation( widget.interactionEndFrictionCoefficient * widget.scaleFactor, scale, details.scaleVelocity / 10, ); final double tFinal = _getFinalTime( details.scaleVelocity.abs(), widget.interactionEndFrictionCoefficient, effectivelyMotionless: 0.1, ); _scaleAnimation = _scaleController.drive( Tween( begin: scale, end: frictionSimulation.x(tFinal), ).chain(CurveTween(curve: Curves.decelerate)), )..addListener(_handleScaleAnimation); _scaleController ..duration = Duration(milliseconds: (tFinal * 1000).round()) ..forward(); case _GestureType.rotate || null: break; } } void _receivedPointerSignal(PointerSignalEvent event) { final Offset local = event.localPosition; final Offset global = event.position; final double scaleChange; if (event is PointerScrollEvent) { if (event.kind == PointerDeviceKind.trackpad) { widget.onInteractionStart?.call( ScaleStartDetails(focalPoint: global, localFocalPoint: local), ); final Offset localDelta = PointerEvent.transformDeltaViaPositions( untransformedEndPosition: global + event.scrollDelta, untransformedDelta: event.scrollDelta, transform: event.transform, ); final Offset focalPointScene = _transformer.toScene(local); final Offset newFocalPointScene = _transformer.toScene( local - localDelta, ); _transformer.value = _matrixTranslate( _transformer.value, newFocalPointScene - focalPointScene, ); widget.onInteractionUpdate?.call( ScaleUpdateDetails( focalPoint: global - event.scrollDelta, localFocalPoint: local - localDelta, focalPointDelta: -localDelta, ), ); widget.onInteractionEnd?.call(ScaleEndDetails()); return; } _handlePointerScrollEvent(event); return; } else if (event is PointerScaleEvent) { scaleChange = event.scale; } else { return; } widget.onInteractionStart?.call( ScaleStartDetails(focalPoint: global, localFocalPoint: local), ); if (!_gestureIsSupported(_GestureType.scale)) { widget.onInteractionUpdate?.call( ScaleUpdateDetails( focalPoint: global, localFocalPoint: local, scale: scaleChange, ), ); widget.onInteractionEnd?.call(ScaleEndDetails()); return; } final Offset focalPointScene = _transformer.toScene(local); _transformer.value = _matrixScale(_transformer.value, scaleChange); // After scaling, translate such that the event's position is at the // same scene point before and after the scale. final Offset focalPointSceneScaled = _transformer.toScene(local); _transformer.value = _matrixTranslate( _transformer.value, focalPointSceneScaled - focalPointScene, ); widget.onInteractionUpdate?.call( ScaleUpdateDetails( focalPoint: global, localFocalPoint: local, scale: scaleChange, ), ); widget.onInteractionEnd?.call(ScaleEndDetails()); } void _handlePointerScrollEvent(PointerScrollEvent event) { final Offset local = event.localPosition; final Offset global = event.position; if (_gestureIsSupported(_GestureType.scale)) { if (HardwareKeyboard.instance.isControlPressed) { _handleMouseWheelScale(event, local, global); return; } final shift = HardwareKeyboard.instance.isShiftPressed; if (shift || HardwareKeyboard.instance.isAltPressed) { _handleMouseWheelPanAsScale(event, local, global, shift); return; } widget.pointerSignalFallback?.call(event); } widget.onInteractionUpdate?.call( ScaleUpdateDetails( focalPoint: global, localFocalPoint: local, scale: math.exp(-event.scrollDelta.dy / widget.scaleFactor), ), ); widget.onInteractionEnd?.call(ScaleEndDetails()); } void _handleMouseWheelScale( PointerScrollEvent event, Offset local, Offset global, ) { final double scaleChange = math.exp( -event.scrollDelta.dy / widget.scaleFactor, ); final Offset focalPointScene = _transformer.toScene(local); _transformer.value = _matrixScale(_transformer.value, scaleChange); final Offset focalPointSceneScaled = _transformer.toScene(local); _transformer.value = _matrixTranslate( _transformer.value, focalPointSceneScaled - focalPointScene, ); widget.onInteractionUpdate?.call( ScaleUpdateDetails( focalPoint: global, localFocalPoint: local, scale: scaleChange, ), ); widget.onInteractionEnd?.call(ScaleEndDetails()); } void _handleMouseWheelPanAsScale( PointerScrollEvent event, Offset local, Offset global, bool flip, ) { final Offset translation = flip ? event.scrollDelta.flip : event.scrollDelta; final Offset focalPointScene = _transformer.toScene(local); final Offset newFocalPointScene = _transformer.toScene(local - translation); _transformer.value = _matrixTranslate( _transformer.value, newFocalPointScene - focalPointScene, ); widget.onTranslate(); } void _handleInertiaAnimation() { if (!_controller.isAnimating) { _currentAxis = null; _animation?.removeListener(_handleInertiaAnimation); _animation = null; _controller.reset(); return; } final Vector3 translationVector = _transformer.value.getTranslation(); final Offset translation = Offset(translationVector.x, translationVector.y); _transformer.value = _matrixTranslate( _transformer.value, _transformer.toScene(_animation!.value) - _transformer.toScene(translation), ); } void _handleScaleAnimation() { if (!_scaleController.isAnimating) { _currentAxis = null; _scaleAnimation?.removeListener(_handleScaleAnimation); _scaleAnimation = null; _scaleController.reset(); return; } final double desiredScale = _scaleAnimation!.value; final double scaleChange = desiredScale / _transformer.value.getMaxScaleOnAxis(); final Offset referenceFocalPoint = _transformer.toScene( _scaleAnimationFocalPoint, ); _transformer.value = _matrixScale(_transformer.value, scaleChange); final Offset focalPointSceneScaled = _transformer.toScene( _scaleAnimationFocalPoint, ); _transformer.value = _matrixTranslate( _transformer.value, focalPointSceneScaled - referenceFocalPoint, ); } void _handleTransformation() { setState(() {}); } void _onPointerDown(PointerDownEvent event) { widget.onPointerDown?.call(event); _scaleGestureRecognizer.addPointer(event); } @override void initState() { super.initState(); _scaleGestureRecognizer = ScaleGestureRecognizer( debugOwner: this, dragStartBehavior: .start, allowedButtonsFilter: (buttons) => buttons == kPrimaryButton, trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), trackpadScrollCausesScale: widget.trackpadScrollCausesScale, ) ..gestureSettings = gestureSettings ..onStart = _onScaleStart ..onUpdate = _onScaleUpdate ..onEnd = _onScaleEnd; _controller = AnimationController(vsync: this); _scaleController = AnimationController(vsync: this); _transformer.addListener(_handleTransformation); } @override void didUpdateWidget(MouseInteractiveViewer oldWidget) { super.didUpdateWidget(oldWidget); final TransformationController? newController = widget.transformationController; if (newController == oldWidget.transformationController) { return; } _transformer.removeListener(_handleTransformation); if (oldWidget.transformationController == null) { _transformer.dispose(); } _transformer = newController ?? TransformationController(); _transformer.addListener(_handleTransformation); } @override void dispose() { _scaleGestureRecognizer.dispose(); _controller.dispose(); _scaleController.dispose(); _transformer.removeListener(_handleTransformation); if (widget.transformationController == null) { _transformer.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { assert(widget.child.key == widget.childKey); return Listener( key: _parentKey, behavior: HitTestBehavior.opaque, onPointerSignal: _receivedPointerSignal, onPointerDown: _onPointerDown, onPointerPanZoomStart: _scaleGestureRecognizer.addPointerPanZoom, onPointerPanZoomUpdate: widget.onPointerPanZoomUpdate, onPointerPanZoomEnd: widget.onPointerPanZoomEnd, child: _InteractiveViewerBuilt( childKey: widget.childKey, clipBehavior: widget.clipBehavior, constrained: widget.constrained, matrix: _transformer.value, alignment: widget.alignment, child: widget.child, ), ); } } class _InteractiveViewerBuilt extends StatelessWidget { const _InteractiveViewerBuilt({ required this.child, required this.childKey, required this.clipBehavior, required this.constrained, required this.matrix, required this.alignment, }); final Widget child; final GlobalKey childKey; final Clip clipBehavior; final bool constrained; final Matrix4 matrix; final Alignment? alignment; @override Widget build(BuildContext context) { Widget child = Transform( transform: matrix, alignment: alignment, child: this.child, ); if (!constrained) { child = OverflowBox( alignment: Alignment.topLeft, minWidth: 0.0, minHeight: 0.0, maxWidth: double.infinity, maxHeight: double.infinity, child: child, ); } if (clipBehavior != Clip.none) { child = ClipRect(clipBehavior: clipBehavior, child: child); } return child; } } enum _GestureType { pan, scale, rotate } double _getFinalTime( double velocity, double drag, { double effectivelyMotionless = 10, }) { return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); } Offset _getMatrixTranslation(Matrix4 matrix) { final Vector3 nextTranslation = matrix.getTranslation(); return Offset(nextTranslation.x, nextTranslation.y); } Quad _transformViewport(Matrix4 matrix, Rect viewport) { final Matrix4 inverseMatrix = matrix.clone()..invert(); return Quad.points( inverseMatrix.transform3( Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0), ), inverseMatrix.transform3( Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0), ), inverseMatrix.transform3( Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0), ), inverseMatrix.transform3( Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0), ), ); } Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { final Matrix4 rotationMatrix = Matrix4.identity() ..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1) ..rotateZ(rotation) ..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1); final Quad boundariesRotated = Quad.points( rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)), rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)), rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)), rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)), ); // ignore: invalid_use_of_visible_for_testing_member return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); } Offset _exceedsBy(Quad boundary, Quad viewport) { final List viewportPoints = [ viewport.point0, viewport.point1, viewport.point2, viewport.point3, ]; Offset largestExcess = Offset.zero; for (final Vector3 point in viewportPoints) { // ignore: invalid_use_of_visible_for_testing_member final Vector3 pointInside = InteractiveViewer.getNearestPointInside( point, boundary, ); final Offset excess = Offset( pointInside.x - point.x, pointInside.y - point.y, ); if (excess.dx.abs() > largestExcess.dx.abs()) { largestExcess = Offset(excess.dx, largestExcess.dy); } if (excess.dy.abs() > largestExcess.dy.abs()) { largestExcess = Offset(largestExcess.dx, excess.dy); } } return _round(largestExcess); } Offset _round(Offset offset) { return Offset( double.parse(offset.dx.toStringAsFixed(9)), double.parse(offset.dy.toStringAsFixed(9)), ); } Offset _alignAxis(Offset offset, Axis axis) { return switch (axis) { Axis.horizontal => Offset(offset.dx, 0.0), Axis.vertical => Offset(0.0, offset.dy), }; } Axis? _getPanAxis(Offset point1, Offset point2) { if (point1 == point2) { return null; } final double x = point2.dx - point1.dx; final double y = point2.dy - point1.dy; return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; } extension on Offset { Offset get flip => Offset(dy, dx); } ================================================ FILE: lib/common/widgets/gesture/tap_gesture_recognizer.dart ================================================ import 'package:flutter/gestures.dart' show TapGestureRecognizer; class NoDeadlineTapGestureRecognizer extends TapGestureRecognizer { NoDeadlineTapGestureRecognizer({ super.debugOwner, super.supportedDevices, super.allowedButtonsFilter, super.preAcceptSlopTolerance, super.postAcceptSlopTolerance, }); @override Duration? get deadline => null; } ================================================ FILE: lib/common/widgets/image/cached_network_svg_image.dart ================================================ // code from cached_network_svg_image; import 'dart:developer'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; class CachedNetworkSVGImage extends StatefulWidget { CachedNetworkSVGImage( String url, { Key? key, String? cacheKey, Widget? placeholder, Widget? errorWidget, double? width, double? height, Map? headers, BoxFit fit = BoxFit.contain, AlignmentGeometry alignment = Alignment.center, bool matchTextDirection = false, bool allowDrawingOutsideViewBox = false, String? semanticsLabel, bool excludeFromSemantics = false, SvgTheme theme = const SvgTheme(), ColorFilter? colorFilter, WidgetBuilder? placeholderBuilder, BaseCacheManager? cacheManager, }) : _url = url, _cacheKey = cacheKey, _placeholder = placeholder, _errorWidget = errorWidget, _width = width, _height = height, _headers = headers, _fit = fit, _alignment = alignment, _matchTextDirection = matchTextDirection, _allowDrawingOutsideViewBox = allowDrawingOutsideViewBox, _semanticsLabel = semanticsLabel, _excludeFromSemantics = excludeFromSemantics, _theme = theme, _colorFilter = colorFilter, _placeholderBuilder = placeholderBuilder, _cacheManager = cacheManager ?? DefaultCacheManager(), super(key: key ?? ValueKey(cacheKey ?? url)); final String _url; final String? _cacheKey; final Widget? _placeholder; final Widget? _errorWidget; final double? _width; final double? _height; final Map? _headers; final BoxFit _fit; final AlignmentGeometry _alignment; final bool _matchTextDirection; final bool _allowDrawingOutsideViewBox; final String? _semanticsLabel; final bool _excludeFromSemantics; final SvgTheme _theme; final ColorFilter? _colorFilter; final WidgetBuilder? _placeholderBuilder; final BaseCacheManager _cacheManager; @override State createState() => _CachedNetworkSVGImageState(); static String _generateKeyFromUrl(String url) => url.split('?').first; } class _CachedNetworkSVGImageState extends State { bool _isLoading = false; bool _isError = false; String? _svgString; late final String _cacheKey; double? height; late TextScaler textScaler; static final _sizeRegExp = RegExp( r'height="([\d\.]+)([c-x]{2})?"', ); @override void initState() { super.initState(); _cacheKey = widget._cacheKey ?? CachedNetworkSVGImage._generateKeyFromUrl(widget._url); _loadImage(); } @override void didChangeDependencies() { super.didChangeDependencies(); textScaler = MediaQuery.textScalerOf(context); } Future _loadImage() async { try { final file = await widget._cacheManager.getSingleFile( widget._url, key: _cacheKey, headers: widget._headers ?? const {}, ); final svg = await file.readAsString(); _svgString = svg; if (widget._width == null && widget._height == null) { final match = _sizeRegExp.firstMatch(svg); if (match != null) { double h = double.parse(match.group(1)!); final suffix = match.group(2); if (suffix != null) { h *= switch (suffix) { 'em' => textScaler.scale(widget._theme.fontSize), 'ex' => textScaler.scale(widget._theme.xHeight), 'pt' => 1.25, 'pc' => 15.0, 'mm' => 3.543307, 'cm' => 35.43307, 'in' => 90.0, _ => 1.0, }; } height = h; } } _isLoading = false; _setState(); } catch (e) { if (kDebugMode) log('CachedNetworkSVGImage: $e'); _isError = true; _isLoading = false; _setState(); } } void _setState() { if (mounted) { setState(() {}); } else { SchedulerBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() {}); } }); } } @override Widget build(BuildContext context) { return SizedBox( width: widget._width, height: widget._height, child: _buildImage(), ); } Widget? _buildImage() { if (_isLoading) return _buildPlaceholderWidget(); if (_isError) return _buildErrorWidget(); return _buildSVGImage(); } Widget _buildPlaceholderWidget() => Center(child: widget._placeholder); Widget _buildErrorWidget() => Center(child: widget._errorWidget); Widget? _buildSVGImage() { if (_svgString == null) { return Center(child: widget._placeholderBuilder?.call(context)); } return SvgPicture.string( _svgString!, fit: widget._fit, width: widget._width, height: widget._height ?? height, alignment: widget._alignment, matchTextDirection: widget._matchTextDirection, allowDrawingOutsideViewBox: widget._allowDrawingOutsideViewBox, semanticsLabel: widget._semanticsLabel, excludeFromSemantics: widget._excludeFromSemantics, colorFilter: widget._colorFilter, placeholderBuilder: widget._placeholderBuilder, theme: widget._theme, ); } } ================================================ FILE: lib/common/widgets/image/image_save.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; void imageSaveDialog({ required String? title, required String? cover, dynamic aid, String? bvid, }) { final double imgWidth = MediaQuery.sizeOf(Get.context!).shortestSide - 16; SmartDialog.show( animationType: SmartAnimationType.centerScale_otherSlide, builder: (context) { const iconSize = 20.0; final theme = Theme.of(context); return Container( width: imgWidth, margin: const .symmetric(horizontal: StyleString.safeSpace), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: StyleString.mdRadius, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Stack( clipBehavior: Clip.none, children: [ GestureDetector( onTap: SmartDialog.dismiss, child: NetworkImgLayer( src: cover, quality: 100, width: imgWidth, height: imgWidth / StyleString.aspectRatio16x9, borderRadius: const .vertical(top: StyleString.imgRadius), ), ), Positioned( right: 8, top: 8, width: 30, height: 30, child: IconButton( tooltip: '关闭', style: IconButton.styleFrom( padding: .zero, backgroundColor: Colors.black.withValues(alpha: 0.3), ), onPressed: SmartDialog.dismiss, icon: const Icon( Icons.close, size: 18, color: Colors.white, ), ), ), ], ), Padding( padding: const EdgeInsets.fromLTRB(12, 10, 8, 10), child: Row( children: [ if (title != null) Expanded( child: SelectableText( title, style: theme.textTheme.titleSmall, ), ) else const Spacer(), if (aid != null || bvid != null) iconButton( iconSize: iconSize, tooltip: '稍后再看', onPressed: () => { SmartDialog.dismiss(), UserHttp.toViewLater(aid: aid, bvid: bvid), }, icon: const Icon(Icons.watch_later_outlined), ), if (cover != null && cover.isNotEmpty) ...[ if (PlatformUtils.isMobile) iconButton( iconSize: iconSize, tooltip: '分享', onPressed: () { SmartDialog.dismiss(); ImageUtils.onShareImg(cover); }, icon: const Icon(Icons.share), ), iconButton( iconSize: iconSize, tooltip: '保存封面图', onPressed: () async { bool saveStatus = await ImageUtils.downloadImg([cover]); if (saveStatus) { SmartDialog.dismiss(); } }, icon: const Icon(Icons.download), ), ], ], ), ), ], ), ); }, ); } ================================================ FILE: lib/common/widgets/image/network_img_layer.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; class NetworkImgLayer extends StatelessWidget { const NetworkImgLayer({ super.key, required this.src, required this.width, required this.height, this.type = .def, this.fadeOutDuration = const Duration(milliseconds: 120), this.fadeInDuration = const Duration(milliseconds: 120), this.quality = 1, this.borderRadius = StyleString.mdRadius, this.getPlaceHolder, this.fit = .cover, this.alignment = .center, this.cacheWidth, }); final String? src; final double width; final double height; final ImageType type; final Duration fadeOutDuration; final Duration fadeInDuration; final int quality; final BorderRadius borderRadius; final ValueGetter? getPlaceHolder; final BoxFit fit; final Alignment alignment; final bool? cacheWidth; static Color? reduceLuxColor = Pref.reduceLuxColor; static bool reduce = false; @override Widget build(BuildContext context) { final isEmote = type == ImageType.emote; final isAvatar = type == ImageType.avatar; if (src?.isNotEmpty == true) { Widget child = _buildImage(context, isEmote: isEmote, isAvatar: isAvatar); if (isEmote) { return child; } else if (isAvatar) { return ClipOval(child: child); } else { return ClipRRect(borderRadius: borderRadius, child: child); } } else { return getPlaceHolder?.call() ?? _placeholder(context, isEmote: isEmote, isAvatar: isAvatar); } } Widget _buildImage( BuildContext context, { required bool isEmote, required bool isAvatar, }) { int? memCacheWidth, memCacheHeight; if (cacheWidth ?? width <= height) { memCacheWidth = width.cacheSize(context); } else { memCacheHeight = height.cacheSize(context); } return CachedNetworkImage( imageUrl: ImageUtils.thumbnailUrl(src, quality), width: width, height: height, memCacheWidth: memCacheWidth, memCacheHeight: memCacheHeight, fit: fit, alignment: alignment, fadeOutDuration: fadeOutDuration, fadeInDuration: fadeInDuration, filterQuality: FilterQuality.low, placeholder: (_, _) => getPlaceHolder?.call() ?? _placeholder(context, isEmote: isEmote, isAvatar: isAvatar), errorWidget: (_, _, _) => _placeholder(context, isEmote: isEmote, isAvatar: isAvatar), colorBlendMode: reduce ? BlendMode.modulate : null, color: reduce ? reduceLuxColor : null, ); } Widget _placeholder( BuildContext context, { required bool isEmote, required bool isAvatar, }) { return Container( width: width, height: height, clipBehavior: isEmote ? Clip.none : Clip.antiAlias, decoration: BoxDecoration( shape: isAvatar ? BoxShape.circle : BoxShape.rectangle, color: Theme.of( context, ).colorScheme.onInverseSurface.withValues(alpha: 0.4), borderRadius: isEmote || isAvatar ? null : borderRadius, ), child: Center( child: Image.asset( isAvatar ? 'assets/images/noface.jpeg' : 'assets/images/loading.png', width: width, height: height, cacheWidth: width.cacheSize(context), colorBlendMode: reduce ? BlendMode.modulate : null, color: reduce ? reduceLuxColor : null, ), ), ); } } ================================================ FILE: lib/common/widgets/image_grid/image_grid_builder.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:collection' show HashSet; import 'dart:math' as math; import 'package:PiliPlus/common/constants.dart' show StyleString; import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart' show ImageModel; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/gestures.dart' show TapGestureRecognizer, LongPressGestureRecognizer; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ContainerRenderObjectMixin, MultiChildLayoutParentData, RenderBoxContainerDefaultsMixin, RenderObjectWithLayoutCallbackMixin, Constraints, LayoutCallback, BoxHitTestResult, BoxHitTestEntry, ContainerParentDataMixin, InformationCollector, DiagnosticsDebugCreator; /// ref [LayoutBuilder] const space = 5.0; typedef ImageGridInfo = ({int column, int row, Size size}); class ImageGridBuilder extends RenderObjectWidget { const ImageGridBuilder({ super.key, required this.picArr, required this.onTap, required this.onSecondaryTapUp, required this.onLongPressStart, required this.builder, }); final List picArr; final ValueChanged onTap; final OnShowMenu? onSecondaryTapUp; final OnShowMenu? onLongPressStart; final List Function(BuildContext context, ImageGridInfo imageGridInfo) builder; @protected bool updateShouldRebuild(ImageGridBuilder oldWidget) => true; @override ImageGridRenderObjectElement createElement() => ImageGridRenderObjectElement(this); @override RenderObject createRenderObject(BuildContext context) { return RenderImageGrid( onTap: onTap, onSecondaryTapUp: onSecondaryTapUp, onLongPressStart: onLongPressStart, ); } @override void updateRenderObject(BuildContext context, RenderImageGrid renderObject) { renderObject ..onTap = onTap ..onSecondaryTapUp = onSecondaryTapUp ..onLongPressStart = onLongPressStart; } } typedef OnShowMenu = Function(int index, Offset offset); class RenderImageGrid extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin, RenderObjectWithLayoutCallbackMixin { RenderImageGrid({ required ValueChanged onTap, required OnShowMenu? onSecondaryTapUp, required OnShowMenu? onLongPressStart, }) : _onTap = onTap, _onSecondaryTapUp = onSecondaryTapUp, _onLongPressStart = onLongPressStart { _tapGestureRecognizer = TapGestureRecognizer()..onTap = _handleOnTap; if (onSecondaryTapUp != null) { _tapGestureRecognizer.onSecondaryTapUp = _handleSecondaryTapUp; } if (onLongPressStart != null) { _longPressGestureRecognizer = LongPressGestureRecognizer() ..onLongPressStart = _handleLongPressStart; } } ValueChanged _onTap; set onTap(ValueChanged value) { _onTap = value; } OnShowMenu? _onSecondaryTapUp; set onSecondaryTapUp(OnShowMenu? value) { _onSecondaryTapUp = value; } OnShowMenu? _onLongPressStart; set onLongPressStart(OnShowMenu? value) { _onLongPressStart = value; } int? _index; void _handleOnTap() { _onTap(_index!); } void _handleSecondaryTapUp(TapUpDetails details) { _onSecondaryTapUp!(_index!, details.globalPosition); } void _handleLongPressStart(LongPressStartDetails details) { _onLongPressStart!(_index!, details.globalPosition); } @override void setupParentData(RenderBox child) { if (child.parentData is! MultiChildLayoutParentData) { child.parentData = MultiChildLayoutParentData(); } } ImageGridInfo? imageGridInfo; LayoutCallback? _callback; void _updateCallback(LayoutCallback value) { if (value == _callback) { return; } _callback = value; scheduleLayoutCallback(); } @override void layoutCallback() => _callback!(constraints); @protected BoxConstraints get layoutInfo => constraints; @override void performLayout() { final BoxConstraints constraints = this.constraints; runLayoutCallback(); final info = imageGridInfo!; final row = info.row; final column = info.column; final width = info.size.width; final height = info.size.height; final childConstraints = BoxConstraints.tightFor( width: width, height: height, ); RenderBox? child = firstChild; while (child != null) { child.layout(childConstraints); final childParentData = child.parentData as MultiChildLayoutParentData; final index = childParentData.id as int; childParentData.offset = Offset( (space + width) * (index % column), (space + height) * (index ~/ column), ); child = childParentData.nextSibling; } size = constraints.constrainDimensions( width * column + space * (column - 1), height * row + space * (row - 1), ); } @override void paint(PaintingContext context, Offset offset) { defaultPaint(context, offset); } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { RenderBox? child = lastChild; while (child != null) { final childParentData = child.parentData as MultiChildLayoutParentData; final bool isHit = result.addWithPaintOffset( offset: childParentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset transformed) { assert(transformed == position - childParentData.offset); if (child!.size.contains(transformed)) { result.add(BoxHitTestEntry(child, transformed)); return true; } return false; }, ); if (isHit) { _index = childParentData.id as int; return true; } child = childParentData.previousSibling; } _index = null; return false; } @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { if (event is PointerDownEvent) { _tapGestureRecognizer.addPointer(event); _longPressGestureRecognizer?.addPointer(event); } } late final TapGestureRecognizer _tapGestureRecognizer; LongPressGestureRecognizer? _longPressGestureRecognizer; @override void dispose() { _tapGestureRecognizer ..onTap = null ..onSecondaryTapUp = null ..dispose(); _longPressGestureRecognizer ?..onLongPressStart = null ..dispose(); _longPressGestureRecognizer = null; _onSecondaryTapUp = null; _onLongPressStart = null; super.dispose(); } @override bool get isRepaintBoundary => true; // gif repaint } class ImageGridRenderObjectElement extends RenderObjectElement { ImageGridRenderObjectElement(ImageGridBuilder super.widget); @override RenderImageGrid get renderObject { return super.renderObject as RenderImageGrid; } @protected @visibleForTesting Iterable get children => _children!.where((Element child) => !_forgottenChildren.contains(child)); List? _children; // We keep a set of forgotten children to avoid O(n^2) work walking _children // repeatedly to remove children. final Set _forgottenChildren = HashSet(); // @override // BuildScope get buildScope => _buildScope; // late final BuildScope _buildScope = BuildScope( // scheduleRebuild: _scheduleRebuild, // ); // bool _deferredCallbackScheduled = false; // void _scheduleRebuild() { // if (_deferredCallbackScheduled) { // return; // } // final bool deferMarkNeedsLayout = // switch (SchedulerBinding.instance.schedulerPhase) { // SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, // SchedulerPhase.transientCallbacks || // SchedulerPhase.midFrameMicrotasks || // SchedulerPhase.persistentCallbacks => false, // }; // if (!deferMarkNeedsLayout) { // renderObject.scheduleLayoutCallback(); // return; // } // _deferredCallbackScheduled = true; // SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); // } // void _frameCallback(Duration timestamp) { // _deferredCallbackScheduled = false; // // This method is only called when the render tree is stable, if the Element // // is deactivated it will never be reincorporated back to the tree. // if (mounted) { // renderObject.scheduleLayoutCallback(); // } // } @override void insertRenderObjectChild(RenderObject child, IndexedSlot slot) { final ContainerRenderObjectMixin< RenderObject, ContainerParentDataMixin > renderObject = this.renderObject; assert(renderObject.debugValidateChild(child)); renderObject.insert(child, after: slot.value?.renderObject); assert(renderObject == this.renderObject); } @override void moveRenderObjectChild( RenderObject child, IndexedSlot oldSlot, IndexedSlot newSlot, ) { final ContainerRenderObjectMixin< RenderObject, ContainerParentDataMixin > renderObject = this.renderObject; assert(child.parent == renderObject); renderObject.move(child, after: newSlot.value?.renderObject); assert(renderObject == this.renderObject); } @override void removeRenderObjectChild(RenderObject child, Object? slot) { final ContainerRenderObjectMixin< RenderObject, ContainerParentDataMixin > renderObject = this.renderObject; assert(child.parent == renderObject); renderObject.remove(child); assert(renderObject == this.renderObject); } @override void visitChildren(ElementVisitor visitor) { if (_children == null) return; for (final Element child in _children!) { if (!_forgottenChildren.contains(child)) { visitor(child); } } } @override void forgetChild(Element child) { if (_children == null) return; assert(_children!.contains(child)); assert(!_forgottenChildren.contains(child)); _forgottenChildren.add(child); super.forgetChild(child); } bool _debugCheckHasAssociatedRenderObject(Element newChild) { assert(() { if (newChild.renderObject == null) { FlutterError.reportError( FlutterErrorDetails( exception: FlutterError.fromParts([ ErrorSummary( 'The children of `MultiChildRenderObjectElement` must each has an associated render object.', ), ErrorHint( 'This typically means that the `${newChild.widget}` or its children\n' 'are not a subtype of `RenderObjectWidget`.', ), newChild.describeElement( 'The following element does not have an associated render object', ), DiagnosticsDebugCreator(DebugCreator(newChild)), ]), ), ); } return true; }()); return true; } @override Element inflateWidget(Widget newWidget, Object? newSlot) { final Element newChild = super.inflateWidget(newWidget, newSlot); assert(_debugCheckHasAssociatedRenderObject(newChild)); return newChild; } @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); renderObject._updateCallback(_rebuildWithConstraints); // final multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget; // final children = List.filled( // multiChildRenderObjectWidget.children.length, // _NullElement.instance, // ); // Element? previousChild; // for (var i = 0; i < children.length; i += 1) { // final Element newChild = inflateWidget( // multiChildRenderObjectWidget.children[i], // IndexedSlot(i, previousChild), // ); // children[i] = newChild; // previousChild = newChild; // } // _children = children; } @override void update(ImageGridBuilder newWidget) { super.update(newWidget); final multiChildRenderObjectWidget = widget as ImageGridBuilder; assert(widget == newWidget); // _children = updateChildren( // _children, // multiChildRenderObjectWidget.children, // forgottenChildren: _forgottenChildren, // ); // _forgottenChildren.clear(); renderObject._updateCallback(_rebuildWithConstraints); if (newWidget.updateShouldRebuild(multiChildRenderObjectWidget)) { _needsBuild = true; renderObject.scheduleLayoutCallback(); } } @override void markNeedsBuild() { // Calling super.markNeedsBuild is not needed. This Element does not need // to performRebuild since this call already does what performRebuild does, // So the element is clean as soon as this method returns and does not have // to be added to the dirty list or marked as dirty. renderObject.scheduleLayoutCallback(); _needsBuild = true; } @override void performRebuild() { // This gets called if markNeedsBuild() is called on us. // That might happen if, e.g., our builder uses Inherited widgets. // Force the callback to be called, even if the layout constraints are the // same. This is because that callback may depend on the updated widget // configuration, or an inherited widget. renderObject.scheduleLayoutCallback(); _needsBuild = true; super .performRebuild(); // Calls widget.updateRenderObject (a no-op in this case). } @override void unmount() { renderObject._callback = null; super.unmount(); } // The LayoutInfoType that was used to invoke the layout callback with last time, // during layout. The `_previousLayoutInfo` value is compared to the new one // to determine whether [LayoutBuilderBase.builder] needs to be called. BoxConstraints? _previousLayoutInfo; bool _needsBuild = true; static ImageGridInfo _calcGridInfo( List picArr, BoxConstraints layoutInfo, ) { final maxWidth = layoutInfo.maxWidth; double imageWidth; double imageHeight; final length = picArr.length; final isSingle = length == 1; final isFour = length == 4; if (length == 2) { imageWidth = imageHeight = (maxWidth - space) / 2; } else { imageHeight = imageWidth = (maxWidth - 2 * space) / 3; if (isSingle) { final img = picArr.first; final width = img.width; final height = img.height; final ratioWH = width / height; final ratioHW = height / width; imageWidth = ratioWH > 1.5 ? maxWidth : (ratioWH >= 1 || (height > width && ratioHW < 1.5)) ? 2 * imageWidth : 1.5 * imageWidth; if (width != 1) { imageWidth = math.min(imageWidth, width.toDouble()); } imageHeight = imageWidth * math.min(ratioHW, StyleString.imgMaxRatio); } } final int column = isFour ? 2 : 3; final int row = isFour ? 2 : (length / 3).ceil(); return ( row: row, column: column, size: Size(imageWidth, imageHeight), ); } void _rebuildWithConstraints(Constraints _) { final BoxConstraints layoutInfo = renderObject.layoutInfo; @pragma('vm:notify-debugger-on-exception') void updateChildCallback() { List built; try { assert(layoutInfo == renderObject.layoutInfo); built = (widget as ImageGridBuilder).builder( this, renderObject.imageGridInfo = _calcGridInfo( (widget as ImageGridBuilder).picArr, layoutInfo, ), ); } catch (e, stack) { built = [ ErrorWidget.builder( _reportException( ErrorDescription('building $widget'), e, stack, informationCollector: () => [ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), ], ), ), ]; } try { if (_children == null) { final children = List.filled( built.length, _NullElement.instance, ); Element? previousChild; for (var i = 0; i < children.length; i += 1) { final Element newChild = inflateWidget( built[i], IndexedSlot(i, previousChild), ); children[i] = newChild; previousChild = newChild; } _children = children; } else { _children = updateChildren( _children!, built, forgottenChildren: _forgottenChildren, ); } } catch (e, stack) { built = [ ErrorWidget.builder( _reportException( ErrorDescription('building $widget'), e, stack, informationCollector: () => [ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), ], ), ), ]; _children = updateChildren([], built); } finally { _needsBuild = false; _previousLayoutInfo = layoutInfo; _forgottenChildren.clear(); } } final VoidCallback? callback = _needsBuild || (layoutInfo != _previousLayoutInfo) ? updateChildCallback : null; owner!.buildScope(this, callback); } } FlutterErrorDetails _reportException( DiagnosticsNode context, Object exception, StackTrace stack, { InformationCollector? informationCollector, }) { final details = FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: context, informationCollector: informationCollector, ); FlutterError.reportError(details); return details; } class _NullElement extends Element { _NullElement() : super(const _NullWidget()); static _NullElement instance = _NullElement(); @override bool get debugDoingBuild => throw UnimplementedError(); } class _NullWidget extends Widget { const _NullWidget(); @override Element createElement() => throw UnimplementedError(); } ================================================ FILE: lib/common/widgets/image_grid/image_grid_view.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:io' show Platform; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_grid/image_grid_builder.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show HapticFeedback; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; class ImageModel { ImageModel({ required num? width, required num? height, required this.url, this.liveUrl, }) { this.width = width == null || width == 0 ? 1 : width; this.height = height == null || height == 0 ? 1 : height; } late num width; late num height; String url; String? liveUrl; bool? _isLongPic; bool? _isLivePhoto; bool get isLongPic => _isLongPic ??= (height / width) > StyleString.imgMaxRatio; bool get isLivePhoto => _isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true; static bool enableLivePhoto = Pref.enableLivePhoto; } class ImageGridView extends StatelessWidget { const ImageGridView({ super.key, required this.picArr, this.onViewImage, this.fullScreen = false, }); final List picArr; final VoidCallback? onViewImage; final bool fullScreen; static bool horizontalPreview = Pref.horizontalPreview; static final _regex = RegExp(r'/videoV|/dynamicDetail$|/articlePage'); void _onTap(BuildContext context, int index) { final imgList = picArr.map( (item) { bool isLive = item.isLivePhoto; return SourceModel( sourceType: isLive ? .livePhoto : .networkImage, url: item.url, liveUrl: isLive ? item.liveUrl : null, width: isLive ? item.width.toInt() : null, height: isLive ? item.height.toInt() : null, isLongPic: item.isLongPic, ); }, ).toList(); if (horizontalPreview && !fullScreen && Get.currentRoute.startsWith(_regex) && !context.mediaQuerySize.isPortrait) { final scaffoldState = Scaffold.maybeOf(context); if (scaffoldState != null) { onViewImage?.call(); PageUtils.onHorizontalPreviewState( scaffoldState, imgList, index, ); return; } } PageUtils.imageView( initialPage: index, imgList: imgList, tag: hashCode.toString(), ); } static BorderRadius _borderRadius( int col, int length, int index, { Radius r = StyleString.imgRadius, }) { if (length == 1) return StyleString.mdRadius; final bool hasUp = index - col >= 0; final bool hasDown = index + col < length; final bool isRowStart = (index % col) == 0; final bool isRowEnd = (index % col) == col - 1 || index == length - 1; final bool hasLeft = !isRowStart; final bool hasRight = !isRowEnd && (index + 1) < length; return BorderRadius.only( topLeft: !hasUp && !hasLeft ? r : Radius.zero, topRight: !hasUp && !hasRight ? r : Radius.zero, bottomLeft: !hasDown && !hasLeft ? r : Radius.zero, bottomRight: !hasDown && !hasRight ? r : Radius.zero, ); } static bool enableImgMenu = Pref.enableImgMenu; void _showMenu(BuildContext context, int index, Offset offset) { HapticFeedback.mediumImpact(); final item = picArr[index]; showMenu( context: context, position: PageUtils.menuPosition(offset), items: [ if (PlatformUtils.isMobile) PopupMenuItem( height: 42, onTap: () => ImageUtils.onShareImg(item.url), child: const Text('分享', style: TextStyle(fontSize: 14)), ), PopupMenuItem( height: 42, onTap: () => ImageUtils.downloadImg([item.url]), child: const Text('保存图片', style: TextStyle(fontSize: 14)), ), if (PlatformUtils.isDesktop) PopupMenuItem( height: 42, onTap: () => PageUtils.launchURL(item.url), child: const Text('网页打开', style: TextStyle(fontSize: 14)), ) else if (picArr.length > 1) PopupMenuItem( height: 42, onTap: () => ImageUtils.downloadImg(picArr.map((item) => item.url).toList()), child: const Text('保存全部', style: TextStyle(fontSize: 14)), ), if (item.isLivePhoto) PopupMenuItem( height: 42, onTap: () => ImageUtils.downloadLivePhoto( url: item.url, liveUrl: item.liveUrl!, width: item.width.toInt(), height: item.height.toInt(), ), child: Text( '保存${Platform.isIOS ? '实况' : '视频'}', style: const TextStyle(fontSize: 14), ), ), ], ); } @override Widget build(BuildContext context) { return Padding( padding: const .only(top: 6), child: ImageGridBuilder( picArr: picArr, onTap: (index) => _onTap(context, index), onSecondaryTapUp: enableImgMenu && PlatformUtils.isDesktop ? (index, offset) => _showMenu(context, index, offset) : null, onLongPressStart: enableImgMenu && PlatformUtils.isMobile ? (index, offset) => _showMenu(context, index, offset) : null, builder: (BuildContext context, ImageGridInfo info) { final width = info.size.width; final height = info.size.height; late final placeHolder = Container( width: width, height: height, decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.onInverseSurface.withValues(alpha: 0.4), ), child: Image.asset( 'assets/images/loading.png', width: width, height: height, cacheWidth: width.cacheSize(context), ), ); return List.generate(picArr.length, (index) { final item = picArr[index]; final borderRadius = _borderRadius( info.column, picArr.length, index, ); Widget child = Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ NetworkImgLayer( src: item.url, width: width, height: height, borderRadius: borderRadius, alignment: item.isLongPic ? .topCenter : .center, cacheWidth: item.width <= item.height, getPlaceHolder: () => placeHolder, ), if (item.isLivePhoto) const PBadge( text: 'Live', right: 8, bottom: 8, type: PBadgeType.gray, ) else if (item.isLongPic) const PBadge( text: '长图', right: 8, bottom: 8, ), ], ); if (!item.isLongPic) { child = Hero( tag: '${item.url}$hashCode', child: child, ); } return LayoutId( id: index, child: child, ); }); }, ), ); } } ================================================ FILE: lib/common/widgets/image_viewer/gallery_viewer.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:io' show File, Platform; import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/image.dart'; import 'package:PiliPlus/common/widgets/image_viewer/loading_indicator.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Image, PageView, LayoutBuilder; import 'package:flutter/services.dart' show HapticFeedback; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; /// /// created by dom on 2026/02/14 /// class GalleryViewer extends StatefulWidget { const GalleryViewer({ super.key, this.minScale = 1.0, this.maxScale = 8.0, required this.quality, required this.sources, this.initIndex = 0, this.onPageChanged, this.tag = '', }); final double minScale; final double maxScale; final int quality; final List sources; final int initIndex; final ValueChanged? onPageChanged; final String tag; @override State createState() => _GalleryViewerState(); } class _GalleryViewerState extends State with SingleTickerProviderStateMixin { late Size _containerSize; late final int _quality; late final RxInt _currIndex; GlobalKey? _key; late bool _hasInit = false; Player? _player; VideoController? _videoController; late final PageController _pageController; late final TapGestureRecognizer _tapGestureRecognizer; late final DoubleTapGestureRecognizer _doubleTapGestureRecognizer; late final ImageHorizontalDragGestureRecognizer _horizontalDragGestureRecognizer; late final LongPressGestureRecognizer _longPressGestureRecognizer; late final AnimationController _animateController; late final Animation _opacityAnimation; double dx = 0, dy = 0; Offset _offset = Offset.zero; bool _dragging = false; String _getActualUrl(String url) { return _quality != 100 ? ImageUtils.thumbnailUrl(url, _quality) : url.http2https; } Future _initPlayer() async { assert(_player == null); final player = await Player.create(); _videoController = await VideoController.create(player); if (!mounted) { player.dispose(); _videoController = null; return; } _player = player; final currItem = widget.sources[_currIndex.value]; if (currItem.sourceType == .livePhoto) { player.open(Media(currItem.liveUrl!)); _currIndex.refresh(); } } @override void initState() { super.initState(); _quality = Pref.previewQ; _currIndex = widget.initIndex.obs; final item = widget.sources[widget.initIndex]; _playIfNeeded(item); if (!item.isLongPic) { _key = GlobalKey(); WidgetsBinding.instance.addPostFrameCallback((_) => _key = null); } _pageController = PageController(initialPage: widget.initIndex); final gestureSettings = MediaQuery.maybeGestureSettingsOf(Get.context!); _tapGestureRecognizer = TapGestureRecognizer() // ..onTap = _onTap ..gestureSettings = gestureSettings; if (PlatformUtils.isDesktop) { _tapGestureRecognizer.onSecondaryTapUp = _showDesktopMenu; } _doubleTapGestureRecognizer = DoubleTapGestureRecognizer() ..onDoubleTap = () {} ..gestureSettings = gestureSettings; _horizontalDragGestureRecognizer = ImageHorizontalDragGestureRecognizer(); _longPressGestureRecognizer = LongPressGestureRecognizer() ..onLongPress = _onLongPress ..gestureSettings = gestureSettings; Future.delayed(const Duration(milliseconds: 300), () { if (mounted) { _tapGestureRecognizer.onTap = _onTap; } }); _animateController = AnimationController( duration: const Duration( milliseconds: 750, ), // reverse only if value <= 0.2 vsync: this, ); _opacityAnimation = _animateController.drive( ColorTween( begin: Colors.black, end: Colors.transparent, ), ); } Matrix4 _onTransform(double val) { final scale = val.lerp(1.0, 0.25); // Matrix4.identity() // ..translateByDouble(size.width / 2, size.height / 2, 0, 1) // ..translateByDouble(size.width * val * dx, size.height * val * dy, 0, 1) // ..scaleByDouble(scale, scale, scale, 1) // ..translateByDouble(-size.width / 2, -size.height / 2, 0, 1); final tmp = (1.0 - scale) / 2.0; return Matrix4.diagonal3Values(scale, scale, scale)..setTranslationRaw( _containerSize.width * (val * dx + tmp), _containerSize.height * (val * dy + tmp), 0, ); } void _updateMoveAnimation() { dy = _offset.dy.sign; if (dy == 0) { dx = 0; } else { dx = _offset.dx / _offset.dy.abs(); } } void _onDragStart(ScaleStartDetails details) { _dragging = true; if (_animateController.isAnimating) { _animateController.stop(); } else { _offset = Offset.zero; _animateController.value = 0.0; } _updateMoveAnimation(); } void _onDragUpdate(ScaleUpdateDetails details) { if (!_dragging || _animateController.isAnimating) { return; } _offset += details.focalPointDelta; _updateMoveAnimation(); if (!_animateController.isAnimating) { _animateController.value = _offset.dy.abs() / _containerSize.height; } } void _onDragEnd(ScaleEndDetails details) { if (!_dragging || _animateController.isAnimating) { return; } _dragging = false; if (!_animateController.isDismissed) { if (_animateController.value > 0.2) { Get.back(); } else { _animateController.reverse(); } } } @override void dispose() { _player?.dispose(); _player = null; _videoController = null; _pageController.dispose(); _animateController.dispose(); _tapGestureRecognizer.dispose(); _doubleTapGestureRecognizer ..onDoubleTapDown = null ..onDoubleTap = null ..dispose(); _longPressGestureRecognizer.dispose(); if (widget.quality != _quality) { for (final item in widget.sources) { if (item.sourceType == SourceType.networkImage) { CachedNetworkImageProvider(_getActualUrl(item.url)).evict(); } } } Future.delayed(const Duration(milliseconds: 200), _currIndex.close); super.dispose(); } void _onPointerDown(PointerDownEvent event) { _tapGestureRecognizer.addPointer(event); _doubleTapGestureRecognizer.addPointer(event); _longPressGestureRecognizer.addPointer(event); } @override Widget build(BuildContext context) { return Listener( behavior: .opaque, onPointerDown: _onPointerDown, child: Stack( fit: .expand, alignment: .center, clipBehavior: .none, children: [ ColoredBoxTransition(color: _opacityAnimation), LayoutBuilder( builder: (context, constraints) { _containerSize = constraints.biggest; return MatrixTransition( alignment: .topLeft, animation: _animateController, onTransform: _onTransform, child: PageView.builder( controller: _pageController, onPageChanged: _onPageChanged, physics: const CustomTabBarViewScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), itemCount: widget.sources.length, itemBuilder: _itemBuilder, horizontalDragGestureRecognizer: () => _horizontalDragGestureRecognizer, ), ); }, ), _buildIndicator, ], ), ); } Widget get _buildIndicator => Positioned( bottom: 0, left: 0, right: 0, child: IgnorePointer( child: Container( padding: MediaQuery.viewPaddingOf(context) + const EdgeInsets.fromLTRB(12, 8, 20, 8), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withValues(alpha: 0.3), ], ), ), alignment: Alignment.center, child: Obx( () => Text( "${_currIndex.value + 1}/${widget.sources.length}", style: const TextStyle(color: Colors.white), ), ), ), ), ); void _playIfNeeded(SourceModel item) { if (item.sourceType == .livePhoto) { if (_player != null) { _player!.open(Media(item.liveUrl!)); } else if (!_hasInit) { _hasInit = true; _initPlayer(); } } } void _onPageChanged(int index) { _player?.pause(); _playIfNeeded(widget.sources[index]); _currIndex.value = index; widget.onPageChanged?.call(index); } late final ValueChanged? _onChangePage = widget.sources.length == 1 ? null : (int offset) { final currPage = _pageController.page?.round() ?? 0; final nextPage = (currPage + offset).clamp( 0, widget.sources.length - 1, ); if (nextPage != currPage) { _pageController.animateToPage( nextPage, duration: const Duration(milliseconds: 200), curve: Curves.ease, ); } }; Widget _itemBuilder(BuildContext context, int index) { final item = widget.sources[index]; final Widget child; switch (item.sourceType) { case SourceType.fileImage: child = Image.file( key: _key, File(item.url), filterQuality: .low, minScale: widget.minScale, maxScale: widget.maxScale, containerSize: _containerSize, onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, doubleTapGestureRecognizer: _doubleTapGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, onChangePage: _onChangePage, ); case SourceType.networkImage: final isLongPic = item.isLongPic; child = Image( key: _key, image: CachedNetworkImageProvider(_getActualUrl(item.url)), minScale: widget.minScale, maxScale: widget.maxScale, containerSize: _containerSize, doubleTapGestureRecognizer: _doubleTapGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, onChangePage: _onChangePage, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded) { return child; } if (frame == null) { if (widget.quality == _quality) { return child; } else { return Image( image: ResizeImage.resizeIfNeeded( _containerSize.width.cacheSize(context), null, CachedNetworkImageProvider( ImageUtils.thumbnailUrl(item.url, widget.quality), ), ), minScale: widget.minScale, maxScale: widget.maxScale, containerSize: _containerSize, onDragStart: null, onDragUpdate: null, onDragEnd: null, doubleTapGestureRecognizer: _doubleTapGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, onChangePage: _onChangePage, ); // final isLongPic = item.isLongPic; // return CachedNetworkImage( // fadeInDuration: Duration.zero, // fadeOutDuration: Duration.zero, // // fit: isLongPic ? .fitWidth : null, // // alignment: isLongPic ? .topCenter : .center, // imageUrl: ImageUtils.thumbnailUrl(item.url, widget.quality), // placeholder: (_, _) => const SizedBox.expand(), // ); } } return child; }, loadingBuilder: loadingBuilder, onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, ); if (isLongPic) { return child; } case SourceType.livePhoto: child = Obx( key: _key, () => _currIndex.value == index && _videoController != null ? Viewer( minScale: widget.minScale, maxScale: widget.maxScale, containerSize: _containerSize, childSize: _containerSize, onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, doubleTapGestureRecognizer: _doubleTapGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, onChangePage: _onChangePage, child: FittedBox( child: SimpleVideo( controller: _videoController!, fill: Colors.transparent, ), ), ) : const SizedBox.shrink(), ); } return Hero(tag: '${item.url}${widget.tag}', child: child); } void _onTap() { EasyThrottle.throttle( 'VIEWER_TAP', const Duration(milliseconds: 555), Get.back, ); } void _onLongPress() { final item = widget.sources[_currIndex.value]; if (item.sourceType == .fileImage) return; HapticFeedback.mediumImpact(); showDialog( context: context, builder: (context) => AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: Column( mainAxisSize: MainAxisSize.min, children: [ if (PlatformUtils.isMobile) ListTile( onTap: () { Get.back(); ImageUtils.onShareImg(item.url); }, dense: true, title: const Text('分享', style: TextStyle(fontSize: 14)), ), ListTile( onTap: () { Get.back(); Utils.copyText(item.url); }, dense: true, title: const Text('复制链接', style: TextStyle(fontSize: 14)), ), ListTile( onTap: () { Get.back(); ImageUtils.downloadImg([item.url]); }, dense: true, title: const Text('保存图片', style: TextStyle(fontSize: 14)), ), if (PlatformUtils.isDesktop) ListTile( onTap: () { Get.back(); PageUtils.launchURL(item.url); }, dense: true, title: const Text('网页打开', style: TextStyle(fontSize: 14)), ) else if (widget.sources.length > 1) ListTile( onTap: () { Get.back(); ImageUtils.downloadImg( widget.sources.map((item) => item.url).toList(), ); }, dense: true, title: const Text('保存全部图片', style: TextStyle(fontSize: 14)), ), if (item.sourceType == SourceType.livePhoto) ListTile( onTap: () { Get.back(); ImageUtils.downloadLivePhoto( url: item.url, liveUrl: item.liveUrl!, width: item.width!, height: item.height!, ); }, dense: true, title: Text( '保存${Platform.isIOS ? ' Live Photo' : '视频'}', style: const TextStyle(fontSize: 14), ), ), ], ), ), ); } void _showDesktopMenu(TapUpDetails details) { final item = widget.sources[_currIndex.value]; if (item.sourceType == .fileImage) return; showMenu( context: context, position: PageUtils.menuPosition(details.globalPosition), items: [ PopupMenuItem( height: 42, onTap: () => Utils.copyText(item.url), child: const Text('复制链接', style: TextStyle(fontSize: 14)), ), PopupMenuItem( height: 42, onTap: () => ImageUtils.downloadImg([item.url]), child: const Text('保存图片', style: TextStyle(fontSize: 14)), ), PopupMenuItem( height: 42, onTap: () => PageUtils.launchURL(item.url), child: const Text('网页打开', style: TextStyle(fontSize: 14)), ), if (item.sourceType == SourceType.livePhoto) PopupMenuItem( height: 42, onTap: () => ImageUtils.downloadLivePhoto( url: item.url, liveUrl: item.liveUrl!, width: item.width!, height: item.height!, ), child: const Text('保存视频', style: TextStyle(fontSize: 14)), ), ], ); } Widget loadingBuilder( BuildContext context, Widget child, ImageChunkEvent? loadingProgress, ) { return Stack( fit: .expand, alignment: .center, clipBehavior: .none, children: [ child, if (loadingProgress != null && loadingProgress.expectedTotalBytes != null && loadingProgress.cumulativeBytesLoaded != loadingProgress.expectedTotalBytes) Center( child: LoadingIndicator( size: 39.4, progress: loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!, ), ), ], ); } } ================================================ FILE: lib/common/widgets/image_viewer/hero.dart ================================================ import 'package:flutter/widgets.dart'; Widget fromHero({ required Object tag, required Widget child, }) => Hero( tag: tag, createRectTween: createEndRectTween, child: child, ); RectTween createEndRectTween(Rect? begin, Rect? end) { if (begin != null && end != null) { final endWidth = end.width; final endHeight = end.height; // TODO: use real image rect final beginRect = Rect.fromLTWH( begin.left + (begin.width - endWidth) / 2, begin.top + (begin.height - endHeight) / 2, endWidth, endHeight, ); return RectTween(begin: beginRect, end: end); } return RectTween(begin: begin, end: end); } ================================================ FILE: lib/common/widgets/image_viewer/hero_dialog_route.dart ================================================ import 'package:flutter/material.dart'; /// https://github.com/qq326646683/interactiveviewer_gallery /// A [PageRoute] with a semi transparent background. /// /// Similar to calling [showDialog] except it can be used with a [Navigator] to /// show a [Hero] animation. class HeroDialogRoute extends PageRoute { HeroDialogRoute({ required this.pageBuilder, }); final RoutePageBuilder pageBuilder; @override bool get opaque => false; @override bool get barrierDismissible => false; @override String? get barrierLabel => null; @override Duration get transitionDuration => const Duration(milliseconds: 300); @override bool get maintainState => true; @override Color? get barrierColor => null; @override Widget buildTransitions( BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) { return FadeTransition( opacity: animation.drive(CurveTween(curve: Curves.easeOut)), child: child, ); } @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return Semantics( scopesRoute: true, explicitChildNodes: true, child: pageBuilder(context, animation, secondaryAnimation), ); } } ================================================ FILE: lib/common/widgets/image_viewer/image.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io' show File; import 'dart:math' as math; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DoubleTapGestureRecognizer; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/semantics.dart'; class Image extends StatefulWidget { const Image({ super.key, required this.image, this.frameBuilder, this.loadingBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, this.filterQuality = FilterQuality.medium, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }); Image.network( String src, { super.key, double scale = 1.0, this.frameBuilder, this.loadingBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.filterQuality = FilterQuality.medium, this.isAntiAlias = false, Map? headers, int? cacheWidth, int? cacheHeight, WebHtmlElementStrategy webHtmlElementStrategy = WebHtmlElementStrategy.never, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, NetworkImage( src, scale: scale, headers: headers, webHtmlElementStrategy: webHtmlElementStrategy, ), ), assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); Image.file( File file, { super.key, double scale = 1.0, this.frameBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, this.filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : assert( !kIsWeb, 'Image.file is not supported on Flutter Web. ' 'Consider using either Image.asset or Image.network instead.', ), image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, FileImage(file, scale: scale), ), loadingBuilder = null, assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); Image.asset( String name, { super.key, AssetBundle? bundle, this.frameBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, double? scale, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, String? package, this.filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, scale != null ? ExactAssetImage( name, bundle: bundle, scale: scale, package: package, ) : AssetImage(name, bundle: bundle, package: package), ), loadingBuilder = null, assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); Image.memory( Uint8List bytes, { super.key, double scale = 1.0, this.frameBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, this.filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale), ), loadingBuilder = null, assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); final ImageProvider image; final ImageFrameBuilder? frameBuilder; final ImageLoadingBuilder? loadingBuilder; final ImageErrorWidgetBuilder? errorBuilder; final double? width; final double? height; final Color? color; final Animation? opacity; final FilterQuality filterQuality; final BlendMode? colorBlendMode; final BoxFit? fit; final AlignmentGeometry alignment; final ImageRepeat repeat; final Rect? centerSlice; final bool matchTextDirection; final bool gaplessPlayback; final String? semanticLabel; final bool excludeFromSemantics; final bool isAntiAlias; final double minScale; final double maxScale; final Size containerSize; final ValueChanged? onDragStart; final ValueChanged? onDragUpdate; final ValueChanged? onDragEnd; final ValueChanged? onChangePage; final DoubleTapGestureRecognizer doubleTapGestureRecognizer; final ImageHorizontalDragGestureRecognizer horizontalDragGestureRecognizer; @override State createState() => _ImageState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('image', image)) ..add(DiagnosticsProperty('frameBuilder', frameBuilder)) ..add( DiagnosticsProperty('loadingBuilder', loadingBuilder), ) ..add(DoubleProperty('width', width, defaultValue: null)) ..add(DoubleProperty('height', height, defaultValue: null)) ..add(ColorProperty('color', color, defaultValue: null)) ..add( DiagnosticsProperty?>( 'opacity', opacity, defaultValue: null, ), ) ..add( EnumProperty( 'colorBlendMode', colorBlendMode, defaultValue: null, ), ) ..add(EnumProperty('fit', fit, defaultValue: null)) ..add( DiagnosticsProperty( 'alignment', alignment, defaultValue: null, ), ) ..add( EnumProperty( 'repeat', repeat, defaultValue: ImageRepeat.noRepeat, ), ) ..add( DiagnosticsProperty( 'centerSlice', centerSlice, defaultValue: null, ), ) ..add( FlagProperty( 'matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction', ), ) ..add( StringProperty('semanticLabel', semanticLabel, defaultValue: null), ) ..add( DiagnosticsProperty( 'this.excludeFromSemantics', excludeFromSemantics, ), ) ..add(EnumProperty('filterQuality', filterQuality)); } } class _ImageState extends State with WidgetsBindingObserver { ImageStream? _imageStream; ImageInfo? _imageInfo; ImageChunkEvent? _loadingProgress; bool _isListeningToStream = false; int? _frameNumber; bool _wasSynchronouslyLoaded = false; late DisposableBuildContext> _scrollAwareContext; Object? _lastException; StackTrace? _lastStack; ImageStreamCompleterHandle? _completerHandle; bool _isPaused = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _scrollAwareContext = DisposableBuildContext>(this); } @override void dispose() { assert(_imageStream != null); WidgetsBinding.instance.removeObserver(this); _stopListeningToStream(); _completerHandle?.dispose(); _scrollAwareContext.dispose(); _replaceImage(info: null); super.dispose(); } @override void didChangeDependencies() { _resolveImage(); _isPaused = !TickerMode.valuesOf(context).enabled || (MediaQuery.maybeDisableAnimationsOf(context) ?? false); if (_isPaused && _frameNumber != null) { _stopListeningToStream(keepStreamAlive: true); } else { _listenToStream(); } super.didChangeDependencies(); } @override void didUpdateWidget(Image oldWidget) { super.didUpdateWidget(oldWidget); if (_isListeningToStream && (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) { final ImageStreamListener oldListener = _getListener(); _imageStream!.addListener(_getListener(recreateListener: true)); _imageStream!.removeListener(oldListener); } if (widget.image != oldWidget.image) { _resolveImage(); _listenToStream(); } } @override void reassemble() { _resolveImage(); super.reassemble(); } void _resolveImage() { final provider = ScrollAwareImageProvider( context: _scrollAwareContext, imageProvider: widget.image, ); final ImageStream newStream = provider.resolve( createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, ), ); _updateSourceStream(newStream); } ImageStreamListener? _imageStreamListener; ImageStreamListener _getListener({bool recreateListener = false}) { if (_imageStreamListener == null || recreateListener) { _lastException = null; _lastStack = null; _imageStreamListener = ImageStreamListener( _handleImageFrame, onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, onError: widget.errorBuilder != null || kDebugMode ? (Object error, StackTrace? stackTrace) { setState(() { _lastException = error; _lastStack = stackTrace; }); assert(() { if (widget.errorBuilder == null) { throw error; } return true; }()); } : null, ); } return _imageStreamListener!; } void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _replaceImage(info: imageInfo); _loadingProgress = null; _lastException = null; _lastStack = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall; }); if (_isPaused) { _stopListeningToStream(keepStreamAlive: true); } } void _handleImageChunk(ImageChunkEvent event) { assert(widget.loadingBuilder != null); setState(() { _loadingProgress = event; _lastException = null; _lastStack = null; }); } void _replaceImage({required ImageInfo? info}) { final ImageInfo? oldImageInfo = _imageInfo; if (oldImageInfo != null) { SchedulerBinding.instance.addPostFrameCallback( (Duration duration) => oldImageInfo.dispose(), debugLabel: 'Image.disposeOldInfo', ); } _imageInfo = info; } void _updateSourceStream(ImageStream newStream) { if (_imageStream?.key == newStream.key) { return; } if (_isListeningToStream) { _imageStream!.removeListener(_getListener()); } if (!widget.gaplessPlayback) { setState(() { _replaceImage(info: null); }); } setState(() { _loadingProgress = null; _frameNumber = null; _wasSynchronouslyLoaded = false; }); _imageStream = newStream; if (_isListeningToStream) { _imageStream!.addListener(_getListener()); } } void _listenToStream() { if (_isListeningToStream) { return; } _isListeningToStream = true; _imageStream!.addListener(_getListener()); _completerHandle?.dispose(); _completerHandle = null; } void _stopListeningToStream({bool keepStreamAlive = false}) { if (!_isListeningToStream) { return; } if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) { _completerHandle = _imageStream!.completer!.keepAlive(); } if (_imageStream!.completer != null && widget.errorBuilder != null) { _imageStream!.completer!.addEphemeralErrorListener( ( Object exception, StackTrace? stackTrace, ) {}, ); } _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } // Widget _debugBuildErrorWidget(BuildContext context, Object error) { // return Stack( // alignment: Alignment.center, // children: [ // const Positioned.fill(child: Placeholder(color: Color(0xCF8D021F))), // Padding( // padding: const EdgeInsets.all(4.0), // child: FittedBox( // child: Text( // '$error', // textAlign: TextAlign.center, // textDirection: TextDirection.ltr, // style: const TextStyle( // shadows: [Shadow(blurRadius: 1.0)], // ), // ), // ), // ), // ], // ); // } @override Widget build(BuildContext context) { if (_lastException != null) { if (widget.errorBuilder != null) { return widget.errorBuilder!(context, _lastException!, _lastStack); } // if (kDebugMode) { // return _debugBuildErrorWidget(context, _lastException!); // } } final Size childSize; final bool isLongPic; double? minScale, maxScale; if (_imageInfo != null) { final imgWidth = _imageInfo!.image.width.toDouble(); final imgHeight = _imageInfo!.image.height.toDouble(); final imgRatio = imgHeight / imgWidth; isLongPic = imgRatio > StyleString.imgMaxRatio && imgHeight > widget.containerSize.height; if (isLongPic) { final compatWidth = math.min(650.0, widget.containerSize.width); minScale = compatWidth / widget.containerSize.height * imgRatio; maxScale = math.max(widget.maxScale, minScale * 3); } childSize = Size(imgWidth, imgHeight); } else { childSize = .zero; isLongPic = false; } Widget result = Viewer( minScale: minScale ?? widget.minScale, maxScale: maxScale ?? widget.maxScale, isLongPic: isLongPic, containerSize: widget.containerSize, childSize: childSize, onDragStart: widget.onDragStart, onDragUpdate: widget.onDragUpdate, onDragEnd: widget.onDragEnd, doubleTapGestureRecognizer: widget.doubleTapGestureRecognizer, horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, onChangePage: widget.onChangePage, child: RawImage(image: _imageInfo?.image), ); if (!widget.excludeFromSemantics) { result = Semantics( container: widget.semanticLabel != null, image: true, label: widget.semanticLabel ?? '', child: result, ); } if (widget.frameBuilder != null) { result = widget.frameBuilder!( context, result, _frameNumber, _wasSynchronouslyLoaded, ); } if (widget.loadingBuilder != null) { result = widget.loadingBuilder!(context, result, _loadingProgress); } return result; } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description ..add(DiagnosticsProperty('stream', _imageStream)) ..add(DiagnosticsProperty('pixels', _imageInfo)) ..add( DiagnosticsProperty( 'loadingProgress', _loadingProgress, ), ) ..add(DiagnosticsProperty('frameNumber', _frameNumber)) ..add( DiagnosticsProperty( 'wasSynchronouslyLoaded', _wasSynchronouslyLoaded, ), ); } } ================================================ FILE: lib/common/widgets/image_viewer/loading_indicator.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' show pi; import 'package:flutter/material.dart'; /// /// created by dom on 2026/02/14 /// class LoadingIndicator extends LeafRenderObjectWidget { const LoadingIndicator({ super.key, required this.size, required this.progress, }); final double size; final double progress; @override RenderObject createRenderObject(BuildContext context) { return RenderLoadingIndicator( preferredSize: size, progress: progress, ); } @override void updateRenderObject( BuildContext context, RenderLoadingIndicator renderObject, ) { renderObject ..preferredSize = size ..progress = progress; } } class RenderLoadingIndicator extends RenderBox { RenderLoadingIndicator({ required double preferredSize, required double progress, }) : _preferredSize = preferredSize, _progress = progress; double _preferredSize; double get preferredSize => _preferredSize; set preferredSize(double value) { if (_preferredSize == value) return; _preferredSize = value; markNeedsLayout(); } double _progress; double get progress => _progress; set progress(double value) { if (_progress == value) return; _progress = value; markNeedsPaint(); } @override void performLayout() { size = constraints.constrainDimensions(_preferredSize, _preferredSize); } @override void paint(PaintingContext context, Offset offset) { if (_progress == 0) { return; } const padding = 8.0; const strokeWidth = 1.4; const startAngle = -pi / 2; final paint = Paint()..isAntiAlias = true; final size = this.size; final radius = size.width / 2 - strokeWidth; final center = size.center(.zero); context.canvas ..drawCircle( center, radius, paint ..style = .fill ..color = const Color(0x80000000), ) ..drawCircle( center, radius, paint ..style = .stroke ..strokeWidth = strokeWidth ..color = Colors.white, ) ..drawArc( Rect.fromCircle(center: center, radius: radius - padding), startAngle, progress * 2 * pi, true, paint..style = .fill, ); } @override bool get isRepaintBoundary => true; } ================================================ FILE: lib/common/widgets/image_viewer/viewer.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart' show touchSlopH; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/physics.dart' show FrictionSimulation; import 'package:flutter/services.dart' show HardwareKeyboard; /// /// created by dom on 2026/02/14 /// class Viewer extends StatefulWidget { const Viewer({ super.key, required this.minScale, required this.maxScale, this.isLongPic = false, required this.containerSize, required this.childSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, required this.child, }); final double minScale; final double maxScale; final bool isLongPic; final Size containerSize; final Size childSize; final Widget child; final ValueChanged? onDragStart; final ValueChanged? onDragUpdate; final ValueChanged? onDragEnd; final ValueChanged? onChangePage; final DoubleTapGestureRecognizer doubleTapGestureRecognizer; final ImageHorizontalDragGestureRecognizer horizontalDragGestureRecognizer; @override State createState() => _ViewerState(); } class _ViewerState extends State with SingleTickerProviderStateMixin { double get _interactionEndFrictionCoefficient => 0.0001 * _scale; // 0.0000135 static const double _scaleFactor = kDefaultMouseScrollToScaleFactor; _GestureType? _gestureType; final Matrix4 _matrix = Matrix4.identity(); late double __scale; double get _scale => __scale; set _scale(double value) { __scale = value; _matrix[0] = _matrix[5] = _matrix[10] = value; } late Offset __position; Offset get _position => __position; set _position(Offset value) { __position = value; _matrix ..[12] = value.dx ..[13] = value.dy; } Offset? _scalePos; double? _scaleStart; Offset? _referenceFocalPoint; late Size _imageSize; late final DoubleTapGestureRecognizer _doubleTapGestureRecognizer; late final ImageHorizontalDragGestureRecognizer _horizontalDragGestureRecognizer; late final ScaleGestureRecognizer _scaleGestureRecognizer; Offset? _downPos; late final AnimationController _animationController; late double _scaleFrom, _scaleTo; late Offset _positionFrom, _positionTo; void _listener() { final t = Curves.easeOut.transform(_animationController.value); _scale = t.lerp(_scaleFrom, _scaleTo); _position = Offset.lerp(_positionFrom, _positionTo, t)!; setState(() {}); } void _reset() { _scale = widget.minScale; _position = .zero; } void _initSize() { _reset(); _imageSize = applyBoxFit( .scaleDown, widget.childSize, widget.containerSize, ).destination; if (widget.isLongPic) { final containerWidth = widget.containerSize.width; final containerHeight = widget.containerSize.height; final imageHeight = _imageSize.height * _scale; _position = Offset( (1 - _scale) * containerWidth / 2, (imageHeight - _scale * containerHeight) / 2, ); } } @override void initState() { super.initState(); _initSize(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), )..addListener(_listener); _doubleTapGestureRecognizer = widget.doubleTapGestureRecognizer; _horizontalDragGestureRecognizer = widget.horizontalDragGestureRecognizer; _scaleGestureRecognizer = ScaleGestureRecognizer(debugOwner: this) ..dragStartBehavior = .start ..onStart = _onScaleStart ..onUpdate = _onScaleUpdate ..onEnd = _onScaleEnd ..gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH); } @override void didUpdateWidget(Viewer oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.containerSize != widget.containerSize || oldWidget.childSize != widget.childSize) { _initSize(); } } @override void dispose() { _animationController ..removeListener(_listener) ..dispose(); _scaleGestureRecognizer.dispose(); super.dispose(); } Offset _toScene(Offset localFocalPoint) { return (localFocalPoint - _position) / _scale; } Offset _clampPosition(Offset offset, double scale) { final containerSize = widget.containerSize; final imageWidth = _imageSize.width * scale; final imageHeight = _imageSize.height * scale; final center = containerSize * (1 - scale) / 2; final dxOffset = (imageWidth - containerSize.width) / 2; final dyOffset = (imageHeight - containerSize.height) / 2; return Offset( imageWidth > containerSize.width ? clampDouble( offset.dx, center.width - dxOffset, center.width + dxOffset, ) : center.width, imageHeight > containerSize.height ? clampDouble( offset.dy, center.height - dyOffset, center.height + dyOffset, ) : center.height, ); } Offset _matrixTranslate(Offset translation) { if (translation == .zero) { return _position; } return _clampPosition(_position + translation * _scale, _scale); } void _onDoubleTapDown(TapDownDetails details) { _downPos = details.localPosition; } void _onDoubleTap() { if (!mounted) return; if (_animationController.isAnimating) return; EasyThrottle.throttle( 'VIEWER_TAP', const Duration(milliseconds: 555), _handleDoubleTap, ); } void _handleDoubleTap() { if (!mounted) return; if (_animationController.isAnimating) return; _scaleFrom = _scale; _positionFrom = _position; double endScale; if (_scale == widget.minScale) { endScale = widget.maxScale * 0.6; if (endScale <= widget.minScale) { endScale = widget.maxScale; } } else { endScale = widget.minScale; } final position = _clampPosition( Offset.lerp(_downPos!, _position, endScale / _scale)!, endScale, ); _scaleTo = endScale; _positionTo = position; _animationController ..duration = const Duration(milliseconds: 300) ..forward(from: 0); } static bool _calc(Offset initialPosition, Offset lastPosition) { final offset = lastPosition - initialPosition; return offset.dy.abs() > offset.dx.abs(); } void _onScaleStart(ScaleStartDetails details) { if (_animationController.isAnimating) { _animationController.stop(); } if (details.pointerCount == 1) { if (widget.isLongPic) { final imageHeight = _scale * _imageSize.height; final containerHeight = widget.containerSize.height; if (_scalePos != null && _calc(_scalePos!, details.focalPoint)) { final bool drag; if (details.focalPoint.dy > _scalePos!.dy) { drag = _position.dy.equals( (imageHeight - _scale * containerHeight) / 2, 1e-6, ); } else { drag = _position.dy.equals(containerHeight - imageHeight, 1e-6); } if (drag) { _gestureType = .drag; widget.onDragStart?.call(details); return; } } } else if (_scale == widget.minScale) { _gestureType = .drag; widget.onDragStart?.call(details); return; } } _scaleStart = _scale; _referenceFocalPoint = _toScene(details.localFocalPoint); } void _onScaleUpdate(ScaleUpdateDetails details) { if (_gestureType == .drag) { widget.onDragUpdate?.call(details); return; } if (details.scale != 1.0) { _gestureType = .scale; _scale = clampDouble( _scaleStart! * details.scale, widget.minScale, widget.maxScale, ); final Offset focalPointSceneScaled = _toScene(details.localFocalPoint); _position = _matrixTranslate( focalPointSceneScaled - _referenceFocalPoint!, ); setState(() {}); } else { _gestureType = .pan; final Offset focalPointScene = _toScene(details.localFocalPoint); final Offset translationChange = focalPointScene - _referenceFocalPoint!; _position = _matrixTranslate(translationChange); _referenceFocalPoint = _toScene(details.localFocalPoint); setState(() {}); } } /// ref [InteractiveViewer] void _onScaleEnd(ScaleEndDetails details) { switch (_gestureType) { case _GestureType.pan: if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { return; } final drag = _interactionEndFrictionCoefficient; final FrictionSimulation frictionSimulationX = FrictionSimulation( drag, _position.dx, details.velocity.pixelsPerSecond.dx, ); final FrictionSimulation frictionSimulationY = FrictionSimulation( drag, _position.dy, details.velocity.pixelsPerSecond.dy, ); final double tFinal = _getFinalTime( details.velocity.pixelsPerSecond.distance, drag, ); final position = _clampPosition( Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), _scale, ); _scaleFrom = _scaleTo = _scale; _positionFrom = _position; _positionTo = position; _animationController ..duration = Duration(milliseconds: (tFinal * 1000).round()) ..forward(from: 0); case _GestureType.scale: // if (details.scaleVelocity.abs() < 0.1) { // return; // } // final double scale = _scale; // final FrictionSimulation frictionSimulation = FrictionSimulation( // _interactionEndFrictionCoefficient * _scaleFactor, // scale, // details.scaleVelocity / 10, // ); // final double tFinal = _getFinalTime( // details.scaleVelocity.abs(), // _interactionEndFrictionCoefficient, // effectivelyMotionless: 0.1, // ); // _scaleAnimation = _scaleController.drive( // Tween( // begin: scale, // end: frictionSimulation.x(tFinal), // ).chain(CurveTween(curve: Curves.decelerate)), // )..addListener(_handleScaleAnimation); // _animationController // ..duration = Duration(milliseconds: (tFinal * 1000).round()) // ..forward(from: 0); break; case _GestureType.drag: widget.onDragEnd?.call(details); case null: } _scalePos = null; _gestureType = null; } @override Widget build(BuildContext context) { return Listener( behavior: .opaque, onPointerDown: _onPointerDown, onPointerPanZoomStart: _onPointerPanZoomStart, onPointerSignal: _onPointerSignal, child: ClipRect( child: Transform( transform: _matrix, child: widget.child, ), ), ); } void _onPointerDown(PointerDownEvent event) { _scalePos = event.position; _doubleTapGestureRecognizer ..onDoubleTapDown = _onDoubleTapDown ..onDoubleTap = _onDoubleTap; _horizontalDragGestureRecognizer ..isBoundaryAllowed = _isBoundaryAllowed ..addPointer(event); _scaleGestureRecognizer.addPointer(event); } void _onPointerPanZoomStart(PointerPanZoomStartEvent event) { _scaleGestureRecognizer.addPointerPanZoom(event); } bool _isBoundaryAllowed(Offset? initialPosition, OffsetPair lastPosition) { if (initialPosition == null) { return true; } if (_scale <= widget.minScale) { return true; } final containerWidth = widget.containerSize.width; final imageWidth = _imageSize.width * _scale; if (imageWidth <= containerWidth) { return true; } final dx = (1 - _scale) * containerWidth / 2; final dxOffset = (imageWidth - containerWidth) / 2; if (initialPosition.dx < lastPosition.global.dx) { return _position.dx.equals(dx + dxOffset, 1e-6); } else { return _position.dx.equals(dx - dxOffset, 1e-6); } } void _onPointerSignal(PointerSignalEvent event) { if (event is PointerScrollEvent) { if (widget.onChangePage != null && !HardwareKeyboard.instance.isControlPressed) { widget.onChangePage!.call(event.scrollDelta.dy < 0 ? -1 : 1); return; } final double scaleChange = math.exp(-event.scrollDelta.dy / _scaleFactor); final Offset local = event.localPosition; final Offset focalPointScene = _toScene(local); _scale = clampDouble( _scale * scaleChange, widget.minScale, widget.maxScale, ); final Offset focalPointSceneScaled = _toScene(local); _position = _matrixTranslate(focalPointSceneScaled - focalPointScene); setState(() {}); } } } enum _GestureType { pan, scale, drag } double _getFinalTime( double velocity, double drag, { double effectivelyMotionless = 10, }) { return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); } ================================================ FILE: lib/common/widgets/keep_alive_wrapper.dart ================================================ import 'package:flutter/material.dart'; class KeepAliveWrapper extends StatefulWidget { const KeepAliveWrapper({ super.key, required this.child, this.wantKeepAlive = true, }); final Widget child; final bool wantKeepAlive; @override State createState() => _KeepAliveWrapperState(); } class _KeepAliveWrapperState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); return widget.child; } @override void didUpdateWidget(KeepAliveWrapper oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.wantKeepAlive != widget.wantKeepAlive) { updateKeepAlive(); } } @override bool get wantKeepAlive => widget.wantKeepAlive; } ================================================ FILE: lib/common/widgets/loading_widget/http_error.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { const HttpError({ super.key, this.isSliver = true, this.errMsg, this.onReload, this.btnText, this.safeArea = true, }); final bool isSliver; final String? errMsg; final VoidCallback? onReload; final String? btnText; final bool safeArea; @override Widget build(BuildContext context) { final theme = Theme.of(context); final child = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 40), SvgPicture.asset( "assets/images/error.svg", height: 200, ), const SizedBox(height: 30), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), child: SelectableText( errMsg ?? '没有数据', textAlign: TextAlign.center, style: theme.textTheme.titleSmall, scrollPhysics: const NeverScrollableScrollPhysics(), ), ), if (onReload != null) FilledButton.tonal( onPressed: onReload, style: FilledButton.styleFrom( tapTargetSize: .padded, backgroundColor: theme.colorScheme.primary.withAlpha(20), shadowColor: Colors.transparent, ), child: Text( btnText ?? '点击重试', style: TextStyle(color: theme.colorScheme.primary), ), ), if (safeArea) SizedBox(height: 40 + MediaQuery.viewPaddingOf(context).bottom), ], ); return isSliver ? SliverToBoxAdapter(child: child) : SizedBox(width: double.infinity, child: child); } } ================================================ FILE: lib/common/widgets/loading_widget/loading_widget.dart ================================================ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/m3e_loading_indicator.dart'; import 'package:flutter/material.dart'; const Widget m3eLoading = Center(child: M3ELoadingIndicator()); const Widget circularLoading = Center(child: CircularProgressIndicator()); const Widget linearLoading = SliverToBoxAdapter( child: LinearProgressIndicator(), ); const Widget scrollableError = CustomScrollView(slivers: [HttpError()]); Widget scrollErrorWidget({ String? errMsg, VoidCallback? onReload, ScrollController? controller, }) => CustomScrollView( controller: controller, slivers: [ HttpError( errMsg: errMsg, onReload: onReload, ), ], ); ================================================ FILE: lib/common/widgets/loading_widget/m3e_loading_indicator.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/physics.dart' show SpringSimulation; import 'package:material_new_shapes/material_new_shapes.dart'; /// reimplement of https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/loading_indicator_m3e class M3ELoadingIndicator extends StatefulWidget { const M3ELoadingIndicator({super.key}); @override State createState() => _M3ELoadingIndicatorState(); } class _M3ELoadingIndicatorState extends State with SingleTickerProviderStateMixin { static final List _morphs = () { final List shapes = [ MaterialShapes.softBurst, MaterialShapes.cookie9Sided, MaterialShapes.pentagon, MaterialShapes.pill, MaterialShapes.sunny, MaterialShapes.cookie4Sided, MaterialShapes.oval, ]; return [ for (var i = 0; i < shapes.length; i++) Morph( shapes[i], shapes[(i + 1) % shapes.length], ), ]; }(); static const int _morphIntervalMs = 650; static const double _fullRotation = 360.0; static const int _globalRotationDurationMs = 4666; static const double _quarterRotation = _fullRotation / 4; late final AnimationController _controller; int _morphIndex = 1; double _morphRotationTargetAngle = _quarterRotation; final _morphAnimationSpec = SpringSimulation( SpringDescription.withDampingRatio(ratio: 0.6, stiffness: 200.0, mass: 1.0), 0.0, 1.0, 5.0, snapToEnd: true, ); void _statusListener(AnimationStatus status) { if (status == AnimationStatus.completed) { _startAnimation(); } } void _startAnimation() { _morphIndex++; _morphRotationTargetAngle = (_morphRotationTargetAngle + _quarterRotation) % _fullRotation; _controller ..value = 0.0 ..animateWith(_morphAnimationSpec); } @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: _morphIntervalMs), ) ..addStatusListener(_statusListener) ..value = 0.0 ..animateWith(_morphAnimationSpec); } @override void dispose() { _controller ..removeStatusListener(_statusListener) ..dispose(); super.dispose(); } double _calcAngle(double progress) { final elapsedInMs = _morphIntervalMs * (_morphIndex - 1) + (_controller.lastElapsedDuration?.inMilliseconds ?? 0); final globalRotationControllerValue = (elapsedInMs % _globalRotationDurationMs) / _globalRotationDurationMs; final globalRotationDegrees = globalRotationControllerValue * _fullRotation; final totalRotationDegrees = progress * _quarterRotation + _morphRotationTargetAngle + globalRotationDegrees; return totalRotationDegrees * (math.pi / 180.0); } @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.secondaryFixedDim; return AnimatedBuilder( animation: _controller, builder: (context, child) { final progress = _controller.value; return _M3ELoadingIndicator( morph: _morphs[_morphIndex % _morphs.length], progress: progress, angle: _calcAngle(progress), color: color, ); }, ); } } class _M3ELoadingIndicator extends LeafRenderObjectWidget { const _M3ELoadingIndicator({ required this.morph, required this.progress, required this.angle, required this.color, }); final Morph morph; final double progress; final double angle; final Color color; @override RenderObject createRenderObject(BuildContext context) { return _RenderM3ELoadingIndicator( morph: morph, progress: progress, angle: angle, color: color, ); } @override void updateRenderObject( BuildContext context, _RenderM3ELoadingIndicator renderObject, ) { renderObject ..morph = morph ..progress = progress ..angle = angle ..color = color; } } class _RenderM3ELoadingIndicator extends RenderBox { _RenderM3ELoadingIndicator({ required Morph morph, required double progress, required double angle, required Color color, }) : _morph = morph, _progress = progress, _angle = angle, _color = color, _paint = Paint() ..style = PaintingStyle.fill ..color = color; Morph _morph; Morph get morph => _morph; set morph(Morph value) { if (_morph == value) return; _morph = value; markNeedsPaint(); } double _progress; double get progress => _progress; set progress(double value) { if (_progress == value) return; _progress = value; markNeedsPaint(); } double _angle; double get angle => _angle; set angle(double value) { if (_angle == value) return; _angle = value; markNeedsPaint(); } Color _color; final Paint _paint; set color(Color value) { if (_color == value) return; _paint.color = _color = value; markNeedsPaint(); } @override void performLayout() { size = constraints.constrainDimensions(40, 40); } @override void paint(PaintingContext context, Offset offset) { final width = size.width; final value = size.width / 2; final matrix = Matrix4.identity() ..translateByDouble(offset.dx + value, offset.dy + value, 0.0, 1.0) ..rotateZ(angle) ..translateByDouble(-value, -value, 0.0, 1.0) ..scaleByDouble(width, width, width, 1.0); final path = morph.toPath(progress: progress).transform(matrix.storage); context.canvas.drawPath(path, _paint); } } ================================================ FILE: lib/common/widgets/loading_widget.dart ================================================ import 'package:PiliPlus/common/widgets/custom_arc.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; class LoadingWidget extends StatelessWidget { const LoadingWidget({ super.key, this.msg = 'loading...', required this.progress, }); ///loading msg final String msg; final RxDouble progress; @override Widget build(BuildContext context) { final theme = Theme.of(context); final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; return Container( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), decoration: BoxDecoration( color: theme.dialogTheme.backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(15)), ), child: Column( spacing: 20, mainAxisSize: MainAxisSize.min, children: [ //loading animation Obx( () => Arc( size: 40, color: onSurfaceVariant, strokeWidth: 3, progress: progress.value, ), ), //msg Text(msg, style: TextStyle(color: onSurfaceVariant)), ], ), ); } } ================================================ FILE: lib/common/widgets/marquee.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; class MarqueeText extends StatelessWidget { final String text; final TextStyle? style; final double spacing; final double velocity; final ContextSingleTicker? provider; const MarqueeText( this.text, { super.key, this.style, this.spacing = 0, this.velocity = 25, this.provider, }); @override Widget build(BuildContext context) { return NormalMarquee( velocity: velocity, spacing: spacing, provider: provider, child: Text( text, style: style, maxLines: 1, textDirection: TextDirection.ltr, ), ); } } abstract class Marquee extends SingleChildRenderObjectWidget { final Axis direction; final Clip clipBehavior; final double spacing; final double velocity; final ContextSingleTicker? provider; const Marquee({ super.key, required this.velocity, required super.child, this.direction = Axis.horizontal, this.clipBehavior = Clip.hardEdge, this.spacing = 0, this.provider, }); @override void updateRenderObject( BuildContext context, covariant MarqueeRender renderObject, ) { renderObject ..direction = direction ..clipBehavior = clipBehavior ..velocity = velocity ..spacing = spacing; if (provider != null) { renderObject.provider = provider!; } } } class NormalMarquee extends Marquee { const NormalMarquee({ super.key, required super.velocity, required super.child, super.direction, super.clipBehavior, super.spacing, super.provider, }); @override RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender( direction: direction, velocity: velocity, clipBehavior: clipBehavior, spacing: spacing, provider: provider ?? ContextSingleTicker(context), ); } class BounceMarquee extends Marquee { const BounceMarquee({ super.key, required super.velocity, required super.child, super.direction, super.clipBehavior, super.spacing, super.provider, }); @override RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender( direction: direction, velocity: velocity, clipBehavior: clipBehavior, spacing: spacing, provider: provider ?? ContextSingleTicker(context), ); } abstract class MarqueeRender extends RenderBox with RenderObjectWithChildMixin { MarqueeRender({ required Axis direction, required double velocity, required double spacing, required this.clipBehavior, required ContextSingleTicker provider, }) : _ticker = provider, _spacing = spacing, _velocity = velocity, _direction = direction, assert(spacing.isFinite && !spacing.isNaN); Clip clipBehavior; Axis _direction; Axis get direction => _direction; set direction(Axis value) { if (_direction == value) return; _direction = value; markNeedsLayout(); } ContextSingleTicker _ticker; set provider(ContextSingleTicker value) { if (_ticker == value) return; if (_ticker._ticker != null) { if (value._ticker != null) { value._ticker!.absorbTicker(_ticker._ticker!); } else { value ..createTicker(_onTick) ..initStart(); } } _ticker.cancel(); _ticker = value; } double _velocity; set velocity(double value) { if (_velocity == value) return; _velocity = value; _simulation = _simulation?.copyWith(initialValue: _delta, velocity: value); _ticker.reset(); } double _spacing; set spacing(double value) { if (value.isNegative) { value *= _direction == Axis.horizontal ? -size.width : -size.height; } if (_spacing == value) return; _simulation = _simulation?.copyWith( initialValue: _delta, addSize: value - _spacing, ); _spacing = value; _ticker.reset(); } double _delta = 0; set delta(double value) { if (_delta == value) return; _delta = value; markNeedsPaint(); } @override void attach(PipelineOwner owner) { super.attach(owner); _ticker.updateTicker(); } @override void dispose() { _ticker.cancel(); super.dispose(); } late double _distance; _MarqueeSimulation? _simulation; @override void performLayout() { final child = this.child; if (child == null) { size = constraints.smallest; return; } if (_direction == Axis.horizontal) { child.layout( BoxConstraints(maxHeight: constraints.maxHeight), parentUsesSize: true, ); size = constraints.constrain(child.size); _distance = child.size.width - size.width; if (_spacing.isNegative) _spacing *= -size.width; } else { child.layout( BoxConstraints(maxWidth: constraints.maxWidth), parentUsesSize: true, ); size = constraints.constrain(child.size); _distance = child.size.height - size.height; if (_spacing.isNegative) _spacing *= -size.height; } if (_distance > 0) { updateSize(); _ticker.initIfNeeded(_onTick); markNeedsCompositingBitsUpdate(); } else { _ticker.cancel(); } } @override bool get isRepaintBoundary => _ticker._ticker != null; void paintCenter(PaintingContext context, Offset offset) { if (_direction == Axis.horizontal) { context.paintChild(child!, Offset(offset.dx - _distance / 2, offset.dy)); } else { context.paintChild(child!, Offset(offset.dx, offset.dy - _distance / 2)); } } void _onTick(Duration elapsed) { delta = _simulation!.x( elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond, ); } void updateSize(); } class _BounceMarqueeRender extends MarqueeRender { _BounceMarqueeRender({ required super.direction, required super.velocity, required super.clipBehavior, required super.spacing, required super.provider, }); @override void updateSize() { final size = _distance + _spacing; if (size == _simulation?.size) return; _simulation = _MarqueeSimulation(_delta, size, false, _velocity); } @override void paint(PaintingContext context, Offset offset) { if (child == null) return; if (_distance > 0) { final delta = _spacing / 2.0 - _delta; void paintChild() { if (_direction == Axis.horizontal) { context.paintChild(child!, Offset(offset.dx + delta, offset.dy)); } else { context.paintChild(child!, Offset(offset.dx, offset.dy + delta)); } } if (clipBehavior == Clip.none) { paintChild(); } else { final rect = Rect.fromLTRB(0, 0, size.width, size.height); context.clipRectAndPaint(rect, clipBehavior, rect, paintChild); } } else { context.paintChild(child!, offset); } } } class _NormalMarqueeRender extends MarqueeRender { _NormalMarqueeRender({ required super.direction, required super.velocity, required super.clipBehavior, required super.spacing, required super.provider, }); @override void updateSize() { final size = (_direction == Axis.horizontal ? child!.size.width : child!.size.height) + _spacing; if (size == _simulation?.size) return; _simulation = _MarqueeSimulation(_delta, size, true, _velocity); } @override void paint(PaintingContext context, Offset offset) { final child = this.child; if (child == null) return; if (_distance > 0) { void paintChild() { if (_direction == Axis.horizontal) { final dx = _delta; context.paintChild(child, Offset(offset.dx - dx, offset.dy)); if (dx > _distance) { context.paintChild( child, Offset(offset.dx + _simulation!.size - dx, offset.dy), ); } } else { final dy = _delta; context.paintChild(child, Offset(offset.dx, offset.dy - dy)); if (dy > _distance) { context.paintChild( child, Offset(offset.dx, offset.dy + _simulation!.size - dy), ); } } } if (clipBehavior == Clip.none) { paintChild(); } else { final rect = Rect.fromLTRB(0, 0, size.width, size.height); context.clipRectAndPaint(rect, clipBehavior, rect, paintChild); } } else { context.paintChild(child, offset); } } } class _MarqueeSimulation extends Simulation { _MarqueeSimulation( this.initialValue, this.size, this.notBounce, this.velocity, ); final double initialValue; final double size; final bool notBounce; final double velocity; @override double x(double timeInSeconds) { assert(timeInSeconds >= 0.0); final totalX = initialValue + velocity * timeInSeconds; if (notBounce) return totalX % size; final doublePeriod = 2.0 * size; final doubleX = totalX % doublePeriod; return doubleX < size ? doubleX : doublePeriod - doubleX; } @override double dx(double timeInSeconds) => velocity; @override bool isDone(double timeInSeconds) => false; _MarqueeSimulation copyWith({ final double? initialValue, final double? addSize, final bool? notBounce, final double? velocity, }) => _MarqueeSimulation( initialValue ?? this.initialValue, addSize == null ? size : size + addSize, notBounce ?? this.notBounce, velocity ?? this.velocity, ); } class ContextSingleTicker implements TickerProvider { Ticker? _ticker; BuildContext context; final ValueGetter? autoStart; ContextSingleTicker(this.context, {this.autoStart}); void initStart() { if (autoStart?.call() ?? true) { _ticker?.start(); } } void startIfNeeded() { if (_ticker case final ticker?) { if (!ticker.isActive) { ticker.start(); } } } void initIfNeeded(TickerCallback onTick) { if (_ticker == null) { createTicker(onTick); initStart(); } } @override Ticker createTicker(TickerCallback onTick) { assert(() { if (_ticker == null) { return true; } throw FlutterError.fromParts([ ErrorSummary( '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.', ), ErrorDescription( 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once.', ), ErrorHint( 'If a State is used for multiple AnimationController objects, or if it is passed to other ' 'objects and those objects might use it more than one time in total, then instead of ' 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.', ), ]); }()); _ticker = Ticker( onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null, ); _tickerModeNotifier = TickerMode.getValuesNotifier(context) ..addListener(updateTicker); updateTicker(); // Sets _ticker.mute correctly. return _ticker!; } void reset() { _ticker ?..stop() ..start(); } void cancel() { _ticker?.dispose(); _ticker = null; _tickerModeNotifier?.removeListener(updateTicker); _tickerModeNotifier = null; } ValueListenable? _tickerModeNotifier; void updateTicker() { if (_tickerModeNotifier != null && _ticker != null) { final TickerModeData values = _tickerModeNotifier!.value; _ticker!.muted = !values.enabled; _ticker!.forceFrames = values.forceFrames; } } set muted(bool value) => _ticker?.muted = value; } ================================================ FILE: lib/common/widgets/only_layout_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderProxyBox; import 'package:flutter/scheduler.dart'; typedef LayoutCallback = void Function(Size size); class OnlyLayoutWidget extends SingleChildRenderObjectWidget { const OnlyLayoutWidget({ super.key, super.child, required this.onPerformLayout, }); final LayoutCallback onPerformLayout; @override RenderObject createRenderObject(BuildContext context) => NoRenderLayoutBox(onPerformLayout: onPerformLayout); @override void updateRenderObject( BuildContext context, NoRenderLayoutBox renderObject, ) { super.updateRenderObject(context, renderObject); renderObject.onPerformLayout = onPerformLayout; } } class NoRenderLayoutBox extends RenderProxyBox { NoRenderLayoutBox({required this.onPerformLayout}); LayoutCallback onPerformLayout; @override void performLayout() { super.performLayout(); SchedulerBinding.instance.addPostFrameCallback((_) { onPerformLayout(size); }); } @override void paint(PaintingContext context, Offset offset) {} } ================================================ FILE: lib/common/widgets/pair.dart ================================================ class Pair { Pair({ required this.first, required this.second, }); T first; R second; } class Triple { Triple({ required this.first, required this.second, required this.third, }); T first; R second; S third; } ================================================ FILE: lib/common/widgets/pendant_avatar.dart ================================================ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/avatar_badge_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; class PendantAvatar extends StatelessWidget { final BadgeType _badgeType; final String? avatar; final double size; final double badgeSize; final String? garbPendantImage; final int? roomId; final VoidCallback? onTap; final bool isMemberAvatar; const PendantAvatar({ super.key, required this.avatar, required this.size, this.isMemberAvatar = false, double? badgeSize, bool isVip = false, int? officialType, this.garbPendantImage, this.roomId, this.onTap, }) : _badgeType = officialType == null || officialType < 0 ? isVip ? BadgeType.vip : BadgeType.none : officialType == 0 ? BadgeType.person : officialType == 1 ? BadgeType.institution : BadgeType.none, badgeSize = badgeSize ?? size / 3; static bool showDynDecorate = Pref.showDynDecorate; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; Widget? pendant; if (showDynDecorate && !garbPendantImage.isNullOrEmpty) { final pendantSize = size * 1.75; pendant = Positioned( // -(size * 1.75 - size) / 2 top: -0.375 * size + (isMemberAvatar ? 2 : 0), child: IgnorePointer( child: NetworkImgLayer( type: .emote, width: pendantSize, height: pendantSize, src: garbPendantImage, getPlaceHolder: () => const SizedBox.shrink(), ), ), ); } return Stack( alignment: Alignment.bottomCenter, clipBehavior: Clip.none, children: [ onTap == null ? _buildAvatar(colorScheme, isMemberAvatar) : GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: _buildAvatar(colorScheme, isMemberAvatar), ), ?pendant, if (roomId != null) Positioned( bottom: 0, child: InkWell( onTap: () => PageUtils.toLiveRoom(roomId), child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: const BorderRadius.all(Radius.circular(36)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( size: 16, applyTextScaling: true, Icons.equalizer_rounded, color: colorScheme.onSecondaryContainer, ), Text( '直播中', style: TextStyle( height: 1, fontSize: 13, color: colorScheme.onSecondaryContainer, ), ), ], ), ), ), ) else if (_badgeType != BadgeType.none) _buildBadge(context, colorScheme, isMemberAvatar), ], ); } Widget _buildAvatar(ColorScheme colorScheme, bool isMemberAvatar) => isMemberAvatar ? DecoratedBox( decoration: BoxDecoration( border: Border.all( width: 2, color: colorScheme.surface, ), shape: BoxShape.circle, ), child: Padding( padding: const EdgeInsets.all(2), child: NetworkImgLayer( src: avatar, width: size, height: size, type: ImageType.avatar, ), ), ) : NetworkImgLayer( src: avatar, width: size, height: size, type: ImageType.avatar, ); Widget _buildBadge( BuildContext context, ColorScheme colorScheme, bool isMemberAvatar, ) { final child = switch (_badgeType) { BadgeType.vip => Image.asset( 'assets/images/big-vip.png', width: badgeSize, height: badgeSize, cacheWidth: badgeSize.cacheSize(context), semanticLabel: _badgeType.desc, ), _ => Icon( Icons.offline_bolt, color: _badgeType.color, size: badgeSize, semanticLabel: _badgeType.desc, ), }; final offset = isMemberAvatar ? 2.0 : 0.0; return Positioned( right: offset, bottom: offset, child: IgnorePointer( child: DecoratedBox( decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.surface, ), child: child, ), ), ); } } ================================================ FILE: lib/common/widgets/player_bar.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ContainerRenderObjectMixin, MultiChildLayoutParentData, RenderBoxContainerDefaultsMixin, BoxHitTestResult; class PlayerBar extends MultiChildRenderObjectWidget { const PlayerBar({ super.key, super.children, }); @override RenderObject createRenderObject(BuildContext context) { return RenderBottomBar(); } } class RenderBottomBar extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { @override void setupParentData(RenderBox child) { if (child.parentData is! MultiChildLayoutParentData) { child.parentData = MultiChildLayoutParentData(); } } Matrix4? _transform; @override void performLayout() { _transform = null; final c = constraints.copyWith(maxWidth: .infinity); final RenderBox first = firstChild!..layout(c, parentUsesSize: true); final RenderBox last = lastChild!..layout(c, parentUsesSize: true); final firstSize = first.size; final lastSize = last.size; final firstParentData = first.parentData as MultiChildLayoutParentData; final lastParentData = last.parentData as MultiChildLayoutParentData; final firstWidth = firstSize.width; final lastWidth = lastSize.width; final totalWidth = firstWidth + lastWidth; final maxWidth = constraints.maxWidth; final height = math.max(firstSize.height, lastSize.height); size = constraints.constrainDimensions(maxWidth, height); firstParentData.offset = Offset(0.0, (height - firstSize.height) / 2); if (totalWidth <= maxWidth) { lastParentData.offset = Offset( maxWidth - lastWidth, (height - lastSize.height) / 2, ); } else { final scale = maxWidth / totalWidth; _transform = Matrix4.identity() ..translateByDouble(0.0, height * (1 - scale) / 2, 0.0, 1.0) ..scaleByDouble(scale, scale, scale, 1.0); lastParentData.offset = Offset( (maxWidth - lastWidth * scale) / scale, (height - lastSize.height) / 2, ); } } @override void paint(PaintingContext context, Offset offset) { if (_transform != null) { context.pushTransform( needsCompositing, offset, _transform!, defaultPaint, ); } else { defaultPaint(context, offset); } } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return result.addWithPaintTransform( transform: _transform, position: position, hitTest: (BoxHitTestResult result, Offset position) { return defaultHitTestChildren(result, position: position); }, ); } @override void applyPaintTransform(RenderBox child, Matrix4 transform) { final childParentData = child.parentData! as MultiChildLayoutParentData; final Offset offset = childParentData.offset; if (_transform != null) { transform ..translateByDouble(offset.dx * _transform!.storage[0], offset.dy, 0, 1) ..multiply(_transform!); } else { transform.translateByDouble(offset.dx, offset.dy, 0, 1); } } } ================================================ FILE: lib/common/widgets/progress_bar/audio_video_progress_bar.dart ================================================ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart' show MouseTrackerAnnotation, PointerEnterEventListener, PointerExitEventListener; /// https://github.com/suragch/audio_video_progress_bar /// A progress bar widget to show or set the location of the currently /// playing audio or video content. /// /// This widget does not itself play audio or video content, but you can /// use it in conjunction with an audio plugin. It is a more convenient /// replacement for the Flutter Slider widget. class ProgressBar extends LeafRenderObjectWidget { /// You must set the current audio or video duration [progress] and also /// the [total] duration. Optionally set the [buffered] content progress /// as well. /// /// When a user drags the thumb to a new location you can be notified /// by the [onSeek] callback so that you can update your audio/video player. const ProgressBar({ super.key, required this.progress, required this.total, this.buffered = .zero, this.onSeek, this.onDragStart, this.onDragUpdate, this.onDragEnd, this.barHeight = 5.0, required this.baseBarColor, required this.progressBarColor, required this.bufferedBarColor, this.thumbRadius = 10.0, required this.thumbColor, required this.thumbGlowColor, this.thumbGlowRadius = 30.0, this.thumbCanPaintOutsideBar = true, }); /// The elapsed playing time of the media. /// /// This should not be greater than the [total] time. final Duration progress; /// The total duration of the media. final Duration total; /// The currently buffered content of the media. /// /// This is useful for streamed content. If you are playing a local file /// then you can leave this out. final Duration buffered; /// A callback when user moves the thumb. /// /// When the user moved the thumb on the progress bar this callback will /// run. It will not run until after the user has finished the touch event. /// /// You will get the chosen duration to start playing at which you can pass /// on to your media player. /// /// If you want continuous duration updates as the user moves the thumb, /// see [onDragUpdate], where the provided [ThumbDragDetails] has a /// `timeStamp` with the seek duration on it. final ValueChanged? onSeek; /// A callback when the user starts to move the thumb. /// /// This will be called only once when the drag begins. This provides you /// with the [ThumbDragDetails]. /// /// This method is useful if you are planning to do something like add a time /// label and/or video preview over the thumb and you need to do some /// initialization. /// /// Use [onSeek] if you only want to seek to a new audio position when the /// drag event has finished. final ThumbDragStartCallback? onDragStart; /// A callback when the user is moving the thumb. /// /// This will be called repeatedly as the thumb position changes. This /// provides you with the [ThumbDragDetails], which notify you of the global /// and local positions of the drag event as well as the current thumb /// duration. The current thumb duration will not go beyond [total] or less /// that `Duration.zero` so you can use this information to clamp the drag /// position values. /// /// This method is useful if you are planning to do something like add a time /// label and/or video preview over the thumb and need to update the position /// to stay in sync with the thumb position. /// /// Use [onSeek] if you only want to seek to a new audio position when the /// drag event has finished. final ThumbDragUpdateCallback? onDragUpdate; /// A callback when the user is finished moving the thumb. /// /// This will be called only once when the drag ends. /// /// This method is useful if you are planning to do something like add a time /// label and/or video preview over the thumb and you need to dispose of /// something when the drag is finished. /// /// This method is called directly before [onSeek]. final VoidCallback? onDragEnd; /// The vertical thickness of the progress bar. final double barHeight; /// The color of the progress bar before playback has started. /// /// By default it is a transparent version of your theme's primary color. final Color baseBarColor; /// The color of the progress bar to the left of the current playing /// [progress]. /// /// By default it is your theme's primary color. final Color progressBarColor; /// The color of the progress bar between the [progress] location and the /// [buffered] location. /// /// By default it is a transparent version of your theme's primary color, /// a shade darker than [baseBarColor]. final Color bufferedBarColor; /// The radius of the circle for the moveable progress bar thumb. final double thumbRadius; /// The color of the circle for the moveable progress bar thumb. /// /// By default it is your theme's primary color. final Color thumbColor; /// The color of the pressed-down effect of the moveable progress bar thumb. /// /// By default it is [thumbColor] with an alpha value of 80. final Color thumbGlowColor; /// The radius of the circle for the pressed-down effect of the moveable /// progress bar thumb. /// /// By default it is 30. final double thumbGlowRadius; /// Whether the thumb radius will before the start of the bar when at the /// beginning or after the end of the bar when at the end. /// /// The default is `true` and this means that the thumb will be painted /// outside of the bounds of the widget if there are no side labels. You can /// wrap [ProgressBar] with a `Padding` widget if your layout needs to leave /// some extra room for the thumb. /// /// When set to `false` the thumb will be clamped within the width of the /// bar. This is nice for aligning the thumb with vertical labels at the start /// and end of playback. However, because of the clamping, the thumb won't /// move during audio/video playback when near the ends. Depending on the /// size of the thumb and the length of the song, this usually only lasts /// a few seconds. The progress label still indicates that playback /// is happening during this time, though. final bool thumbCanPaintOutsideBar; @override RenderObject createRenderObject(BuildContext context) { return RenderProgressBar( progress: progress, total: total, buffered: buffered, onSeek: onSeek, onDragStart: onDragStart, onDragUpdate: onDragUpdate, onDragEnd: onDragEnd, barHeight: barHeight, baseBarColor: baseBarColor, progressBarColor: progressBarColor, bufferedBarColor: bufferedBarColor, thumbRadius: thumbRadius, thumbColor: thumbColor, thumbGlowColor: thumbGlowColor, thumbGlowRadius: thumbGlowRadius, thumbCanPaintOutsideBar: thumbCanPaintOutsideBar, ); } @override void updateRenderObject( BuildContext context, RenderProgressBar renderObject, ) { renderObject ..total = total ..progress = progress ..buffered = buffered ..onSeek = onSeek ..onDragStart = onDragStart ..onDragUpdate = onDragUpdate ..onDragEnd = onDragEnd ..barHeight = barHeight ..baseBarColor = baseBarColor ..progressBarColor = progressBarColor ..bufferedBarColor = bufferedBarColor ..thumbRadius = thumbRadius ..thumbColor = thumbColor ..thumbGlowColor = thumbGlowColor ..thumbGlowRadius = thumbGlowRadius ..thumbCanPaintOutsideBar = thumbCanPaintOutsideBar; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(StringProperty('progress', progress.toString())) ..add(StringProperty('total', total.toString())) ..add(StringProperty('buffered', buffered.toString())) ..add( ObjectFlagProperty>( 'onSeek', onSeek, ifNull: 'unimplemented', ), ) ..add( ObjectFlagProperty( 'onDragStart', onDragStart, ifNull: 'unimplemented', ), ) ..add( ObjectFlagProperty( 'onDragUpdate', onDragUpdate, ifNull: 'unimplemented', ), ) ..add( ObjectFlagProperty( 'onDragEnd', onDragEnd, ifNull: 'unimplemented', ), ) ..add(DoubleProperty('barHeight', barHeight)) ..add(ColorProperty('baseBarColor', baseBarColor)) ..add(ColorProperty('progressBarColor', progressBarColor)) ..add(ColorProperty('bufferedBarColor', bufferedBarColor)) ..add(DoubleProperty('thumbRadius', thumbRadius)) ..add(ColorProperty('thumbColor', thumbColor)) ..add(ColorProperty('thumbGlowColor', thumbGlowColor)) ..add(DoubleProperty('thumbGlowRadius', thumbGlowRadius)) ..add( FlagProperty( 'thumbCanPaintOutsideBar', value: thumbCanPaintOutsideBar, ifTrue: 'true', ifFalse: 'false', showName: true, ), ); } } /// The callback signature for when the thumb begins a horizontal drag. typedef ThumbDragStartCallback = void Function(ThumbDragDetails details); /// The callback signature for when the thumb is moving on horizontally and has /// new data. typedef ThumbDragUpdateCallback = void Function(ThumbDragDetails details); /// Data to pass back on drag callback events class ThumbDragDetails { const ThumbDragDetails({ this.timeStamp = Duration.zero, this.globalPosition = Offset.zero, this.localPosition = Offset.zero, }); /// The duration position of the thumb on the progress bar final Duration timeStamp; /// The global position of the drag event moving the thumb on the progress bar. final Offset globalPosition; /// The local position of the drag event moving the thumb on the progress bar. final Offset localPosition; @override String toString() => '${objectRuntimeType(this, 'ThumbDragDetails')}(' 'time: $timeStamp, ' 'global: $globalPosition, ' 'local: $localPosition)'; } // Handles all gestures so that it will always win a the gesture arena. // Without doing this, if you used this widget in a swipable tab layout, // you would cause a swipe rather than a drag when trying to move the thumb. class _EagerHorizontalDragGestureRecognizer extends HorizontalDragGestureRecognizer { @override void addAllowedPointer(PointerDownEvent event) { super.addAllowedPointer(event); resolve(GestureDisposition.accepted); } @override String get debugDescription => '_EagerHorizontalDragGestureRecognizer'; } class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { RenderProgressBar({ required Duration progress, required Duration total, required Duration buffered, ValueChanged? onSeek, ThumbDragStartCallback? onDragStart, ThumbDragUpdateCallback? onDragUpdate, VoidCallback? onDragEnd, required double barHeight, required Color baseBarColor, required Color progressBarColor, required Color bufferedBarColor, double thumbRadius = 20.0, required Color thumbColor, required Color thumbGlowColor, double thumbGlowRadius = 30.0, bool thumbCanPaintOutsideBar = true, }) : _total = total, _buffered = buffered, _onSeek = onSeek, _onDragStartUserCallback = onDragStart, _onDragUpdateUserCallback = onDragUpdate, _onDragEndUserCallback = onDragEnd, _barHeight = barHeight, _baseBarColor = baseBarColor, _progressBarColor = progressBarColor, _bufferedBarColor = bufferedBarColor, _thumbRadius = thumbRadius, _thumbColor = thumbColor, _thumbGlowColor = thumbGlowColor, _thumbGlowRadius = thumbGlowRadius, _paintThumbGlow = thumbGlowRadius > thumbRadius, _thumbCanPaintOutsideBar = thumbCanPaintOutsideBar, _hitTestSelf = onDragStart != null { if (onDragStart != null) { _drag = _EagerHorizontalDragGestureRecognizer() ..onStart = _onDragStart ..onUpdate = _onDragUpdate ..onEnd = _onDragEnd ..onCancel = _finishDrag; } if (!_userIsDraggingThumb) { _progress = progress; _thumbValue = _proportionOfTotal(_progress); } } @override void dispose() { _drag?.dispose(); _drag = null; super.dispose(); } // This is the gesture recognizer used to move the thumb. _EagerHorizontalDragGestureRecognizer? _drag; // This is a value between 0.0 and 1.0 used to indicate the position on // the bar. late double _thumbValue; // The thumb can move for two reasons. One is that the [progress] changed. // The other is that the user is dragging the thumb. This variable keeps // track of that so that while the user is dragging the thumb at the same // time as a [progress] update there won't be a conflict. bool _userIsDraggingThumb = false; void _onDragStart(DragStartDetails details) { if (onDragStart == null) { return; } _userIsDraggingThumb = true; _updateThumbPosition(details.localPosition); onDragStart?.call( ThumbDragDetails( timeStamp: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, ), ); } void _onDragUpdate(DragUpdateDetails details) { if (onDragUpdate == null) { return; } _updateThumbPosition(details.localPosition); onDragUpdate?.call( ThumbDragDetails( timeStamp: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, ), ); } void _onDragEnd(DragEndDetails details) { if (onSeek == null) { return; } onDragEnd?.call(); onSeek?.call(_currentThumbDuration()); _finishDrag(); } void _finishDrag() { _userIsDraggingThumb = false; markNeedsPaint(); } Duration _currentThumbDuration() { final thumbMilliseconds = _thumbValue * total.inMilliseconds; return Duration(milliseconds: thumbMilliseconds.round()); } // This needs to stay in sync with the layout. This could be a potential // source of bugs if there is a layout change but we forget to update this. // It might be a good idea to redesign the architecture so that there is // only one place to make changes. void _updateThumbPosition(Offset localPosition) { final dx = localPosition.dx; // The paint used to draw the bar line draws half of the cap before the // start of the line (and after the end of the line). The cap radius is // equal to half of the line width, which in this case is the bar height. final barCapRadius = _barHeight / 2; double barStart = barCapRadius; double barEnd = size.width - barCapRadius; final barWidth = barEnd - barStart; final position = (dx - barStart).clamp(0.0, barWidth); _thumbValue = (position / barWidth); _progress = _currentThumbDuration(); markNeedsPaint(); } /// The play location of the media. /// /// This is used to update the thumb value and the left time label. Duration get progress => _progress; Duration _progress = Duration.zero; set progress(Duration value) { final clamp = _clampDuration(value); if (_progress == clamp) { return; } if (!_userIsDraggingThumb) { _progress = clamp; _thumbValue = _proportionOfTotal(clamp); } markNeedsPaint(); } /// The total time length of the media. Duration get total => _total; Duration _total; set total(Duration value) { final clamp = (value.isNegative) ? Duration.zero : value; if (_total == clamp) { return; } _total = clamp; if (!_userIsDraggingThumb) { _thumbValue = _proportionOfTotal(progress); } markNeedsPaint(); } /// The buffered length of the media when streaming. Duration get buffered => _buffered; Duration _buffered; set buffered(Duration value) { final clamp = _clampDuration(value); if (_buffered == clamp) { return; } _buffered = clamp; markNeedsPaint(); } Duration _clampDuration(Duration value) { if (value.isNegative) return Duration.zero; if (value.compareTo(_total) > 0) return _total; return value; } /// A callback for the audio duration position to where the thumb was moved. ValueChanged? get onSeek => _onSeek; ValueChanged? _onSeek; set onSeek(ValueChanged? value) { if (value == _onSeek) { return; } _onSeek = value; } /// A callback when the thumb starts being dragged. ThumbDragStartCallback? get onDragStart => _onDragStartUserCallback; ThumbDragStartCallback? _onDragStartUserCallback; set onDragStart(ThumbDragStartCallback? value) { if (value == _onDragStartUserCallback) { return; } _onDragStartUserCallback = value; } /// A callback when the thumb is being dragged. ThumbDragUpdateCallback? get onDragUpdate => _onDragUpdateUserCallback; ThumbDragUpdateCallback? _onDragUpdateUserCallback; set onDragUpdate(ThumbDragUpdateCallback? value) { if (value == _onDragUpdateUserCallback) { return; } _onDragUpdateUserCallback = value; } /// A callback when the thumb drag is finished. VoidCallback? get onDragEnd => _onDragEndUserCallback; VoidCallback? _onDragEndUserCallback; set onDragEnd(VoidCallback? value) { if (value == _onDragEndUserCallback) { return; } _onDragEndUserCallback = value; } /// The vertical thickness of the bar that the thumb moves along. double get barHeight => _barHeight; double _barHeight; set barHeight(double value) { if (_barHeight == value) return; _barHeight = value; markNeedsPaint(); } /// The color of the progress bar before any playing or buffering. Color get baseBarColor => _baseBarColor; Color _baseBarColor; set baseBarColor(Color value) { if (_baseBarColor == value) return; _baseBarColor = value; markNeedsPaint(); } /// The color of the played portion of the progress bar. Color get progressBarColor => _progressBarColor; Color _progressBarColor; set progressBarColor(Color value) { if (_progressBarColor == value) return; _progressBarColor = value; markNeedsPaint(); } /// The color of the visible buffered portion of the progress bar. Color get bufferedBarColor => _bufferedBarColor; Color _bufferedBarColor; set bufferedBarColor(Color value) { if (_bufferedBarColor == value) return; _bufferedBarColor = value; markNeedsPaint(); } /// The color of the moveable thumb. Color get thumbColor => _thumbColor; Color _thumbColor; set thumbColor(Color value) { if (_thumbColor == value) return; _thumbColor = value; markNeedsPaint(); } /// The length of the radius for the circular thumb. double get thumbRadius => _thumbRadius; double _thumbRadius; set thumbRadius(double value) { if (_thumbRadius == value) return; _thumbRadius = value; markNeedsLayout(); } /// The color of the pressed-down effect of the moveable thumb. Color get thumbGlowColor => _thumbGlowColor; Color _thumbGlowColor; set thumbGlowColor(Color value) { if (_thumbGlowColor == value) return; _thumbGlowColor = value; if (_userIsDraggingThumb) markNeedsPaint(); } /// The length of the radius of the pressed-down effect of the moveable thumb. bool _paintThumbGlow; double get thumbGlowRadius => _thumbGlowRadius; double _thumbGlowRadius; set thumbGlowRadius(double value) { if (_thumbGlowRadius == value) return; _thumbGlowRadius = value; _paintThumbGlow = value > _thumbRadius; markNeedsLayout(); } /// Whether the thumb will paint before the start or after the end of the bar. bool get thumbCanPaintOutsideBar => _thumbCanPaintOutsideBar; bool _thumbCanPaintOutsideBar; set thumbCanPaintOutsideBar(bool value) { if (_thumbCanPaintOutsideBar == value) return; _thumbCanPaintOutsideBar = value; markNeedsPaint(); } // The smallest that this widget would ever want to be. static const _minDesiredWidth = 100.0; @override double computeMinIntrinsicWidth(double height) => _minDesiredWidth; @override double computeMaxIntrinsicWidth(double height) => _minDesiredWidth; @override double computeMinIntrinsicHeight(double width) => _heightWhenNoLabels(); @override double computeMaxIntrinsicHeight(double width) => _heightWhenNoLabels(); final bool _hitTestSelf; @override bool hitTestSelf(Offset position) => _hitTestSelf; @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent) { _drag?.addPointer(event); } } @override void performLayout() { size = computeDryLayout(constraints); } @override Size computeDryLayout(BoxConstraints constraints) { final desiredWidth = constraints.maxWidth; final desiredHeight = _heightWhenNoLabels(); return constraints.constrainDimensions(desiredWidth, desiredHeight); } double _heightWhenNoLabels() { return max(2 * _thumbRadius, _barHeight); } @override bool get isRepaintBoundary => true; @override void paint(PaintingContext context, Offset offset) { final canvas = context.canvas ..save() ..translate(offset.dx, offset.dy); _drawProgressBarWithoutLabels(canvas); canvas.restore(); } /// Draw the progress bar without labels like this: /// /// | -------O---------------- | /// void _drawProgressBarWithoutLabels(Canvas canvas) { final barWidth = size.width; final barHeight = _heightWhenNoLabels(); _drawProgressBar(canvas, Offset.zero, Size(barWidth, barHeight)); } void _drawProgressBar(Canvas canvas, Offset offset, Size localSize) { canvas ..save() ..translate(offset.dx, offset.dy); _drawBaseBar(canvas, localSize); _drawBufferedBar(canvas, localSize); _drawCurrentProgressBar(canvas, localSize); _drawThumb(canvas, localSize); canvas.restore(); } void _drawBaseBar(Canvas canvas, Size localSize) { _drawBar( canvas: canvas, availableSize: localSize, widthProportion: 1.0, color: baseBarColor, ); } void _drawBufferedBar(Canvas canvas, Size localSize) { _drawBar( canvas: canvas, availableSize: localSize, widthProportion: _proportionOfTotal(_buffered), color: bufferedBarColor, ); } void _drawCurrentProgressBar(Canvas canvas, Size localSize) { _drawBar( canvas: canvas, availableSize: localSize, widthProportion: _proportionOfTotal(_progress), color: progressBarColor, ); } void _drawBar({ required Canvas canvas, required Size availableSize, required double widthProportion, required Color color, }) { final baseBarPaint = Paint() ..color = color ..strokeCap = StrokeCap.round ..strokeWidth = _barHeight; final capRadius = _barHeight / 2; final adjustedWidth = availableSize.width - barHeight; final dx = widthProportion * adjustedWidth + capRadius; final startPoint = Offset(capRadius, availableSize.height / 2); final endPoint = Offset(dx, availableSize.height / 2); canvas.drawLine(startPoint, endPoint, baseBarPaint); } void _drawThumb(Canvas canvas, Size localSize) { final thumbPaint = Paint()..color = thumbColor; final barCapRadius = _barHeight / 2; final availableWidth = localSize.width - _barHeight; var thumbDx = _thumbValue * availableWidth + barCapRadius; if (!_thumbCanPaintOutsideBar) { thumbDx = thumbDx.clamp(_thumbRadius, localSize.width - _thumbRadius); } final center = Offset(thumbDx, localSize.height / 2); if (_userIsDraggingThumb && _paintThumbGlow) { final thumbGlowPaint = Paint()..color = thumbGlowColor; canvas.drawCircle(center, thumbGlowRadius, thumbGlowPaint); } canvas.drawCircle(center, thumbRadius, thumbPaint); } double _proportionOfTotal(Duration duration) { if (total.inMilliseconds == 0) { return 0.0; } return (duration.inMilliseconds / total.inMilliseconds).clamp(0.0, 1.0); } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); // description config ..textDirection = TextDirection.ltr ..label = '进度条' //'Progress bar'; ..value = '${(_thumbValue * 100).round()}%' // increase action ..onIncrease = increaseAction; final increased = _thumbValue + _semanticActionUnit; config ..increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%' // decrease action ..onDecrease = decreaseAction; final decreased = _thumbValue - _semanticActionUnit; config.decreasedValue = '${((decreased).clamp(0.0, 1.0) * 100).round()}%'; } // This is how much to move the thumb if the move is triggered by a // semantic action rather than a touch event. static const double _semanticActionUnit = 0.05; void increaseAction() { final newValue = _thumbValue + _semanticActionUnit; _thumbValue = (newValue).clamp(0.0, 1.0); onSeek?.call(_currentThumbDuration()); markNeedsPaint(); markNeedsSemanticsUpdate(); } void decreaseAction() { final newValue = _thumbValue - _semanticActionUnit; _thumbValue = (newValue).clamp(0.0, 1.0); onSeek?.call(_currentThumbDuration()); markNeedsPaint(); markNeedsSemanticsUpdate(); } @override MouseCursor get cursor => SystemMouseCursors.click; @override PointerEnterEventListener? onEnter; @override PointerExitEventListener? onExit; @override bool get validForMouseTracker => false; } ================================================ FILE: lib/common/widgets/progress_bar/segment_progress_bar.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:ui' as ui; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show listEquals; import 'package:flutter/gestures.dart' show TapGestureRecognizer; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show BoxHitTestEntry; @immutable sealed class BaseSegment { final double end; const BaseSegment({ required this.end, }); } @immutable class Segment extends BaseSegment { final double start; final Color color; const Segment({ required this.start, required super.end, required this.color, }); @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other is Segment) { return start == other.start && end == other.end && color == other.color; } return false; } @override int get hashCode => Object.hash(start, end, color); } @immutable class ViewPointSegment extends BaseSegment { final String? title; final String? url; final int? from; final int? to; const ViewPointSegment({ required super.end, this.title, this.url, this.from, this.to, }); @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other is ViewPointSegment) { return end == other.end && title == other.title && url == other.url && from == other.from && to == other.to; } return false; } @override int get hashCode => Object.hash(end, title, url, from, to); } class SegmentProgressBar extends BaseSegmentProgressBar { const SegmentProgressBar({ super.key, super.height, required super.segments, }); @override RenderObject createRenderObject(BuildContext context) { return RenderProgressBar( height: height, segments: segments, ); } } class RenderProgressBar extends BaseRenderProgressBar { RenderProgressBar({ required super.height, required super.segments, }); @override void paint(PaintingContext context, Offset offset) { final size = this.size; final canvas = context.canvas; final paint = Paint()..style = PaintingStyle.fill; for (final segment in segments) { paint.color = segment.color; final segmentStart = offset.dx + segment.start * size.width; final segmentEnd = offset.dx + segment.end * size.width; if (segmentEnd > segmentStart || (segmentEnd == segmentStart && segmentStart > 0)) { canvas.drawRect( Rect.fromLTRB( segmentStart, offset.dy, segmentEnd == segmentStart ? segmentStart + 2 : segmentEnd, size.height + offset.dy, ), paint, ); } } } } class ViewPointSegmentProgressBar extends BaseSegmentProgressBar { const ViewPointSegmentProgressBar({ super.key, super.height, required super.segments, this.onSeek, }); final ValueSetter? onSeek; @override RenderObject createRenderObject(BuildContext context) { return RenderViewPointProgressBar( height: height, segments: segments, onSeek: onSeek, ); } @override void updateRenderObject( BuildContext context, RenderViewPointProgressBar renderObject, ) { renderObject ..height = height ..segments = segments ..onSeek = onSeek; } } class RenderViewPointProgressBar extends BaseRenderProgressBar { RenderViewPointProgressBar({ required super.height, required super.segments, ValueSetter? onSeek, }) : _onSeek = onSeek, _hitTestSelf = onSeek != null { if (onSeek != null) { _tapGestureRecognizer = TapGestureRecognizer()..onTapUp = _onTapUp; } } @override void performLayout() { size = constraints.constrainDimensions(constraints.maxWidth, _barHeight); } static const double _barHeight = 15.0; static const double _dividerWidth = 2.0; static ui.Paragraph _getParagraph(String title, double size) { final builder = ui.ParagraphBuilder( ui.ParagraphStyle( textDirection: .ltr, strutStyle: ui.StrutStyle( leading: 0, height: 1, fontSize: size, ), ), ) ..pushStyle( ui.TextStyle( color: Colors.white, fontSize: size, height: 1, ), ) ..addText(title); return builder.build() ..layout(const ui.ParagraphConstraints(width: double.infinity)); } @override void paint(PaintingContext context, Offset offset) { final size = this.size; final canvas = context.canvas; final paint = Paint()..style = PaintingStyle.fill; if (offset != .zero) { canvas ..save() ..translate(offset.dx, offset.dy); } assert(segments.isSortedBy((i) => i.end)); canvas.drawRect( Rect.fromLTRB(0, 0, size.width, _barHeight), paint..color = Colors.grey[600]!.withValues(alpha: 0.45), ); paint.color = Colors.black.withValues(alpha: 0.5); double prevEnd = 0.0; for (final segment in segments) { final segmentEnd = segment.end * size.width; canvas.drawRect( Rect.fromLTRB( segmentEnd, 0, segmentEnd + _dividerWidth, _barHeight + height, ), paint, ); final title = segment.title; if (title != null && title.isNotEmpty) { final segmentWidth = segmentEnd - prevEnd; final paragraph = _getParagraph(title, 10); final textWidth = paragraph.maxIntrinsicWidth; final textHeight = paragraph.height; final isOverflow = textWidth > segmentWidth; final Offset offset; if (isOverflow) { final scale = segmentWidth / textWidth; canvas ..save() ..translate(prevEnd, (_barHeight - textHeight * scale) / 2) ..scale(scale); offset = Offset.zero; } else { offset = Offset( (segmentWidth - textWidth) / 2 + prevEnd, (_barHeight - textHeight) / 2, ); } canvas.drawParagraph(paragraph, offset); paragraph.dispose(); if (isOverflow) { canvas.restore(); } } prevEnd = segmentEnd + _dividerWidth; } if (offset != .zero) canvas.restore(); } ValueSetter? _onSeek; set onSeek(ValueSetter? value) { if (_onSeek == value) { return; } _onSeek = value; } TapGestureRecognizer? _tapGestureRecognizer; @override void dispose() { _onSeek = null; _tapGestureRecognizer ?..onTapUp = null ..dispose(); _tapGestureRecognizer = null; super.dispose(); } final bool _hitTestSelf; @override bool hitTestSelf(Offset position) => _hitTestSelf; @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { if (event is PointerDownEvent) { _tapGestureRecognizer?.addPointer(event); } } @pragma('vm:notify-debugger-on-exception') void _onTapUp(TapUpDetails details) { try { final seg = details.localPosition.dx / size.width; final item = _segments[_segments.lowerBoundByKey((i) => i.end, seg)]; if (item.from case final from?) { _onSeek?.call(Duration(seconds: from)); } // if (kDebugMode) debugPrint('${item.title},,${item.from}'); } catch (_) {} } } abstract class BaseSegmentProgressBar extends LeafRenderObjectWidget { const BaseSegmentProgressBar({ super.key, this.height = 3.5, required this.segments, }); final double height; final List segments; @override void updateRenderObject( BuildContext context, BaseRenderProgressBar renderObject, ) { renderObject ..height = height ..segments = segments; } } class BaseRenderProgressBar extends RenderBox { BaseRenderProgressBar({ required double height, required List segments, }) : _height = height, _segments = segments; double _height; double get height => _height; set height(double value) { if (_height == value) return; _height = value; markNeedsLayout(); } List _segments; List get segments => _segments; set segments(List value) { if (listEquals(_segments, value)) return; _segments = value; markNeedsPaint(); } @override void performLayout() { size = constraints.constrainDimensions(constraints.maxWidth, height); } } ================================================ FILE: lib/common/widgets/progress_bar/video_progress_indicator.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'package:flutter/widgets.dart'; class VideoProgressIndicator extends LeafRenderObjectWidget { const VideoProgressIndicator({ super.key, required this.color, required this.backgroundColor, this.radius = 10, this.height = 4, required this.progress, }) : assert(progress >= 0 && progress <= 1); final Color color; final Color backgroundColor; final double radius; final double height; final double progress; @override RenderObject createRenderObject(BuildContext context) { return RenderProgressBar( color: color, backgroundColor: backgroundColor, radius: radius, height: height, progress: progress, ); } @override void updateRenderObject( BuildContext context, RenderProgressBar renderObject, ) { renderObject ..color = color ..backgroundColor = backgroundColor ..radius = radius ..height = height ..progress = progress; } } class RenderProgressBar extends RenderBox { RenderProgressBar({ required Color color, required Color backgroundColor, required double radius, required double height, required double progress, }) : _color = color, _backgroundColor = backgroundColor, _radius = radius, _height = height, _progress = progress; Color _color; Color get color => _color; set color(Color value) { if (_color == value) return; _color = value; markNeedsPaint(); } Color _backgroundColor; Color get backgroundColor => _backgroundColor; set backgroundColor(Color value) { if (_backgroundColor == value) return; _backgroundColor = value; markNeedsPaint(); } double _progress; double get progress => _progress; set progress(double value) { if (_progress == value) return; _progress = value; markNeedsPaint(); } double _radius; double get radius => _radius; set radius(double value) { if (_radius == value) return; _radius = value; markNeedsLayout(); } double _height; double get height => _height; set height(double value) { if (_height == value) return; _height = value; markNeedsPaint(); } @override void performLayout() { size = constraints.constrainDimensions(constraints.maxWidth, _radius); } @override void paint(PaintingContext context, Offset offset) { final size = this.size; final canvas = context.canvas ..save() ..translate(offset.dx, offset.dy); final paint = Paint()..style = .fill; canvas.clipRect( .fromLTRB(0, size.height - height, size.width, size.height), ); final radius = Radius.circular(_radius); final rect = Rect.fromLTRB(0, 0, size.width, size.height); final rrect = RRect.fromRectAndCorners( rect, bottomLeft: radius, bottomRight: radius, ); if (progress == 0) { canvas.drawRRect(rrect, paint..color = _backgroundColor); } else if (progress == 1) { canvas.drawRRect(rrect, paint..color = _color); } else { final w = size.width * progress; final left = Rect.fromLTRB(0, 0, w, size.height); final right = Rect.fromLTRB(w, 0, size.width, size.height); canvas ..clipRRect(rrect) ..drawRect(left, paint..color = _color) ..drawRect(right, paint..color = _backgroundColor); } canvas.restore(); } } ================================================ FILE: lib/common/widgets/radio_widget.dart ================================================ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart'; class RadioWidget extends StatefulWidget { final T value; final String title; final bool tristate; final EdgeInsetsGeometry? padding; final MainAxisSize mainAxisSize; const RadioWidget({ super.key, required this.value, required this.title, this.tristate = false, this.padding, this.mainAxisSize = MainAxisSize.min, }); @override State> createState() => RadioWidgetState(); } class RadioWidgetState extends State> with RadioClient { late final _RadioRegistry _radioRegistry = _RadioRegistry(this); @override final focusNode = FocusNode(); @override T get radioValue => widget.value; bool get checked => radioValue == registry!.groupValue; @override bool get tristate => widget.tristate; @override bool get enabled => registry != null; @override void dispose() { registry = null; focusNode.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); registry = RadioGroup.maybeOf(context); assert(registry != null); } void _handleTap() { if (checked) { if (tristate) registry!.onChanged(null); return; } registry!.onChanged(radioValue); } @override Widget build(BuildContext context) { final child = Row( mainAxisSize: widget.mainAxisSize, children: [ ExcludeFocus( child: Radio( value: radioValue, groupRegistry: _radioRegistry, materialTapTargetSize: PlatformUtils.isDesktop ? .padded : .shrinkWrap, ), ), Text(widget.title), ], ); return InkWell( onTap: _handleTap, focusNode: focusNode, child: widget.padding == null ? child : Padding(padding: widget.padding!, child: child), ); } } class WrapRadioOptionsGroup extends StatelessWidget { final String groupTitle; final Map options; final EdgeInsetsGeometry? itemPadding; const WrapRadioOptionsGroup({ super.key, required this.groupTitle, required this.options, this.itemPadding, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (groupTitle.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 22), child: Text( groupTitle, style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Wrap( children: options.entries.map((entry) { return RadioWidget( value: entry.key, title: entry.value, padding: itemPadding ?? const EdgeInsets.only(right: 10), ); }).toList(), ), ), ], ); } } /// A registry to controls internal [Radio] and hides it from [RadioGroup] /// ancestor. /// /// [RadioListTile] implements the [RadioClient] directly to register to /// [RadioGroup] ancestor. Therefore, it has to hide the internal [Radio] from /// participate in the [RadioGroup] ancestor. class _RadioRegistry extends RadioGroupRegistry { _RadioRegistry(this.state); final RadioWidgetState state; @override T? get groupValue => state.registry!.groupValue; @override ValueChanged get onChanged => state.registry!.onChanged; @override void registerClient(RadioClient radio) {} @override void unregisterClient(RadioClient radio) {} } ================================================ FILE: lib/common/widgets/route_aware_mixin.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; import 'package:get/get_navigation/src/routes/default_route.dart' show GetPageRoute; final routeObserver = RouteObserver(); mixin RouteAwareMixin on State, RouteAware { @override void initState() { super.initState(); routeObserver.subscribe(this, Get.routing.route as GetPageRoute); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } } ================================================ FILE: lib/common/widgets/scale_app.dart ================================================ import 'dart:async' show scheduleMicrotask; import 'dart:collection' show Queue; import 'dart:ui' show PointerDataPacket; import 'package:flutter/gestures.dart' show PointerEventConverter; import 'package:flutter/rendering.dart' show RenderView, ViewConfiguration; import 'package:flutter/widgets.dart'; /// ref https://github.com/LastMonopoly/scaled_app /// Adapted from [WidgetsFlutterBinding] /// class ScaledWidgetsFlutterBinding extends WidgetsFlutterBinding { ScaledWidgetsFlutterBinding._({double scaleFactor = 1.0}) : _scaleFactor = scaleFactor; /// Calculate scale factor from device size. double _scaleFactor; /// Update scaleFactor callback, then rebuild layout set scaleFactor(double scaleFactor) { if (_scaleFactor == scaleFactor) return; _scaleFactor = scaleFactor; handleMetricsChanged(); } double devicePixelRatioScaled = 0; static ScaledWidgetsFlutterBinding? _binding; static ScaledWidgetsFlutterBinding get instance => _binding!; /// Scaling will be applied based on [scaleFactor] callback. /// static WidgetsBinding ensureInitialized({double scaleFactor = 1.0}) => _binding ??= ScaledWidgetsFlutterBinding._(scaleFactor: scaleFactor); /// Override the method from [RendererBinding.createViewConfiguration] to /// change what size or device pixel ratio the [RenderView] will use. /// /// See more: /// * [RendererBinding.createViewConfiguration] /// * [TestWidgetsFlutterBinding.createViewConfiguration] @override ViewConfiguration createViewConfigurationFor(RenderView renderView) { final view = renderView.flutterView; final devicePixelRatio = view.devicePixelRatio; devicePixelRatioScaled = devicePixelRatio * _scaleFactor; final BoxConstraints physicalConstraints = BoxConstraints.fromViewConstraints(view.physicalConstraints); return ViewConfiguration( physicalConstraints: physicalConstraints, logicalConstraints: physicalConstraints / devicePixelRatioScaled, devicePixelRatio: devicePixelRatioScaled, ); } /// Adapted from [GestureBinding.initInstances] @override void initInstances() { super.initInstances(); platformDispatcher.onPointerDataPacket = _handlePointerDataPacket; } @override void unlocked() { super.unlocked(); _flushPointerEventQueue(); } final Queue _pendingPointerEvents = Queue(); /// When we scale UI using [ViewConfiguration], [ui.window] stays the same. /// /// [GestureBinding] uses [platformDispatcher.implicitView.devicePixelRatio] for calculations, /// so we override corresponding methods. /// void _handlePointerDataPacket(PointerDataPacket packet) { // We convert pointer data to logical pixels so that e.g. the touch slop can be // defined in a device-independent manner. try { _pendingPointerEvents.addAll( PointerEventConverter.expand(packet.data, _devicePixelRatioForView), ); if (!locked) { _flushPointerEventQueue(); } } catch (error, stack) { FlutterError.reportError( FlutterErrorDetails( exception: error, stack: stack, library: 'gestures library', context: ErrorDescription('while handling a pointer data packet'), ), ); } } double _devicePixelRatioForView(int viewId) => devicePixelRatioScaled; /// Dispatch a [PointerCancelEvent] for the given pointer soon. /// /// The pointer event will be dispatched before the next pointer event and /// before the end of the microtask but not within this function call. @override void cancelPointer(int pointer) { if (_pendingPointerEvents.isEmpty && !locked) { scheduleMicrotask(_flushPointerEventQueue); } _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer)); } void _flushPointerEventQueue() { assert(!locked); while (_pendingPointerEvents.isNotEmpty) { handlePointerEvent(_pendingPointerEvents.removeFirst()); } } } ================================================ FILE: lib/common/widgets/scroll_behavior.dart ================================================ import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/material.dart'; class CustomScrollBehavior extends MaterialScrollBehavior { const CustomScrollBehavior(this.dragDevices); @override Widget buildScrollbar( BuildContext context, Widget child, ScrollableDetails details, ) => child; @override final Set dragDevices; } const Set desktopDragDevices = { PointerDeviceKind.touch, PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, PointerDeviceKind.trackpad, PointerDeviceKind.unknown, PointerDeviceKind.mouse, }; ================================================ FILE: lib/common/widgets/scroll_physics.dart ================================================ import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart' hide TabBarView; Widget tabBarView({ required List children, TabController? controller, }) => TabBarView( controller: controller, physics: clampingScrollPhysics, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, children: children, ); SpringDescription _customSpringDescription() { final List springDescription = Pref.springDescription; return SpringDescription( mass: springDescription[0], stiffness: springDescription[1], damping: springDescription[2], ); } const clampingScrollPhysics = CustomTabBarViewScrollPhysics( parent: ClampingScrollPhysics(), ); class CustomTabBarViewScrollPhysics extends ScrollPhysics { const CustomTabBarViewScrollPhysics({super.parent}); @override CustomTabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) { return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor)); } static final _springDescription = _customSpringDescription(); @override SpringDescription get spring => _springDescription; } mixin ReloadMixin { late bool reload = false; } class ReloadScrollPhysics extends AlwaysScrollableScrollPhysics { const ReloadScrollPhysics({super.parent, required this.controller}); final ReloadMixin controller; @override ReloadScrollPhysics applyTo(ScrollPhysics? ancestor) { return ReloadScrollPhysics( parent: buildParent(ancestor), controller: controller, ); } @override double adjustPositionForNewDimensions({ required ScrollMetrics oldPosition, required ScrollMetrics newPosition, required bool isScrolling, required double velocity, }) { if (controller.reload) { controller.reload = false; return 0; } return super.adjustPositionForNewDimensions( oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity, ); } } ================================================ FILE: lib/common/widgets/select_mask.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:flutter/material.dart'; Widget selectMask( ThemeData theme, bool checked, { BorderRadiusGeometry borderRadius = StyleString.mdRadius, }) { return AnimatedOpacity( opacity: checked ? 1 : 0, duration: const Duration(milliseconds: 200), child: Container( alignment: Alignment.center, decoration: BoxDecoration( borderRadius: borderRadius, color: Colors.black.withValues(alpha: 0.6), ), child: AnimatedScale( scale: checked ? 1 : 0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: Container( width: 34, height: 34, decoration: BoxDecoration( color: theme.colorScheme.surface.withValues(alpha: 0.8), shape: BoxShape.circle, ), child: Icon( Icons.done_all_outlined, color: theme.colorScheme.primary, semanticLabel: '取消选择', ), ), ), ), ); } ================================================ FILE: lib/common/widgets/self_sized_horizontal_list.dart ================================================ import 'package:PiliPlus/common/widgets/only_layout_widget.dart'; import 'package:flutter/material.dart'; class SelfSizedHorizontalList extends StatefulWidget { const SelfSizedHorizontalList({ super.key, required this.itemCount, required this.itemBuilder, required this.separatorBuilder, this.controller, this.padding, }); final int itemCount; final EdgeInsets? padding; final IndexedWidgetBuilder itemBuilder; final IndexedWidgetBuilder separatorBuilder; final ScrollController? controller; @override State createState() => _SelfSizedHorizontalListState(); } class _SelfSizedHorizontalListState extends State { double? _height; @override Widget build(BuildContext context) { if (_height == null) { return OnlyLayoutWidget( onPerformLayout: (Size size) { if (!mounted) return; _height = size.height; setState(() {}); }, child: Padding( padding: widget.padding ?? .zero, child: widget.itemBuilder(context, 0), ), ); } return SizedBox( height: _height, child: ListView.separated( scrollDirection: .horizontal, padding: widget.padding, itemCount: widget.itemCount, controller: widget.controller, itemBuilder: widget.itemBuilder, separatorBuilder: widget.separatorBuilder, ), ); } } ================================================ FILE: lib/common/widgets/sliver/sliver_floating_header.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderSliverSingleBoxAdapter, SliverGeometry; /// ref [SliverFloatingHeader] class SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget { const SliverFloatingHeaderWidget({ super.key, required Widget super.child, required this.backgroundColor, }); final Color backgroundColor; @override RenderObject createRenderObject(BuildContext context) => RenderSliverFloatingHeader(backgroundColor: backgroundColor); @override void updateRenderObject( BuildContext context, RenderSliverFloatingHeader renderObject, ) { renderObject.backgroundColor = backgroundColor; } } class RenderSliverFloatingHeader extends RenderSliverSingleBoxAdapter { RenderSliverFloatingHeader({ required Color backgroundColor, }) : _backgroundColor = backgroundColor; Color _backgroundColor; set backgroundColor(Color value) { if (_backgroundColor == value) return; _backgroundColor = value; markNeedsPaint(); } double? _childPosition; double? lastScrollOffset; late double effectiveScrollOffset; bool get floatingHeaderNeedsToBeUpdated { return lastScrollOffset != null && (constraints.scrollOffset < lastScrollOffset! || effectiveScrollOffset < child!.size.height); } @override void performLayout() { if (!floatingHeaderNeedsToBeUpdated) { effectiveScrollOffset = constraints.scrollOffset; } else { double delta = lastScrollOffset! - constraints.scrollOffset; // > 0 when the header is growing if (constraints.userScrollDirection == .forward) { final childExtent = child!.size.height; if (effectiveScrollOffset > childExtent) { effectiveScrollOffset = childExtent; // The header is now just above the start edge of viewport. } } else { // delta > 0 and scrolling forward is a contradiction. Assume that it's noise (set delta to 0). delta = clampDouble(delta, -double.infinity, 0); } effectiveScrollOffset = clampDouble( effectiveScrollOffset - delta, 0.0, constraints.scrollOffset, ); } child?.layout(constraints.asBoxConstraints(), parentUsesSize: true); final childExtent = child!.size.height; final double paintExtent = childExtent - effectiveScrollOffset; final double layoutExtent = childExtent - constraints.scrollOffset; geometry = SliverGeometry( paintOrigin: math.min(constraints.overlap, 0.0), scrollExtent: childExtent, paintExtent: clampDouble( paintExtent, 0.0, constraints.remainingPaintExtent, ), layoutExtent: clampDouble( layoutExtent, 0.0, constraints.remainingPaintExtent, ), maxPaintExtent: childExtent, hasVisualOverflow: false, ); _childPosition = math.min(0.0, paintExtent - childExtent); lastScrollOffset = constraints.scrollOffset; } @override double childMainAxisPosition(covariant RenderObject child) { return _childPosition ?? 0; } @override void applyPaintTransform(RenderObject child, Matrix4 transform) { assert(child == this.child); applyPaintTransformForBoxChild(child as RenderBox, transform); } @override void paint(PaintingContext context, Offset offset) { if (child != null && geometry!.visible) { offset += Offset(0.0, childMainAxisPosition(child!)); final size = child!.size; context.canvas.drawRect( Rect.fromLTWH( offset.dx, offset.dy - 2, size.width, size.height + 2, ), Paint()..color = _backgroundColor, ); context.paintChild(child!, offset); } } @override bool hitTestSelf({ required double mainAxisPosition, required double crossAxisPosition, }) => true; } ================================================ FILE: lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/rendering.dart' show RenderSliverSingleBoxAdapter, SliverConstraints, SliverGeometry; import 'package:flutter/widgets.dart'; /// ref [SliverPersistentHeader] class SliverPinnedDynamicHeader extends SingleChildRenderObjectWidget { const SliverPinnedDynamicHeader({ super.key, required Widget super.child, required this.minExtent, required this.maxExtent, }); final double minExtent; final double maxExtent; @override RenderObject createRenderObject(BuildContext context) { return RenderSliverPinnedDynamicHeader( minExtent: minExtent, maxExtent: maxExtent, ); } @override void updateRenderObject( BuildContext context, RenderSliverPinnedDynamicHeader renderObject, ) { renderObject ..minExtent = minExtent ..maxExtent = maxExtent; } } class RenderSliverPinnedDynamicHeader extends RenderSliverSingleBoxAdapter { RenderSliverPinnedDynamicHeader({ required double minExtent, required double maxExtent, }) : _minExtent = minExtent, _maxExtent = maxExtent; double _minExtent; double get minExtent => _minExtent; set minExtent(double value) { if (_minExtent == value) return; _minExtent = value; markNeedsLayout(); } double _maxExtent; double get maxExtent => _maxExtent; set maxExtent(double value) { // removed // if (_maxExtent == value) return; _maxExtent = value; markNeedsLayout(); } @override void performLayout() { final SliverConstraints constraints = this.constraints; final double shrinkOffset = math.min(constraints.scrollOffset, maxExtent); child!.layout( constraints.asBoxConstraints( maxExtent: math.max(minExtent, maxExtent - shrinkOffset), ), parentUsesSize: true, ); final double childExtent = child!.size.height; final double effectiveRemainingPaintExtent = math.max( 0, constraints.remainingPaintExtent - constraints.overlap, ); final double layoutExtent = clampDouble( maxExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent, ); geometry = SliverGeometry( scrollExtent: maxExtent, paintOrigin: constraints.overlap, paintExtent: math.min(childExtent, effectiveRemainingPaintExtent), layoutExtent: layoutExtent, maxPaintExtent: maxExtent, maxScrollObstructionExtent: minExtent, cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent, hasVisualOverflow: false, ); } @override void paint(PaintingContext context, Offset offset) { if (child != null && geometry!.visible) { context.paintChild(child!, offset); } } @override double childMainAxisPosition(RenderBox child) => 0.0; } ================================================ FILE: lib/common/widgets/sliver/sliver_pinned_header.dart ================================================ /* * This file is part of PiliPlus * * PiliPlus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PiliPlus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PiliPlus. If not, see . */ import 'dart:math' as math; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/rendering.dart' show RenderSliverSingleBoxAdapter, SliverGeometry; import 'package:flutter/widgets.dart'; /// ref [SliverPersistentHeader] class SliverPinnedHeader extends SingleChildRenderObjectWidget { const SliverPinnedHeader({ super.key, required Widget super.child, this.backgroundColor, }); final Color? backgroundColor; @override RenderObject createRenderObject(BuildContext context) => RenderSliverPinnedHeader(backgroundColor: backgroundColor); @override void updateRenderObject( BuildContext context, RenderSliverPinnedHeader renderObject, ) { renderObject.backgroundColor = backgroundColor; } } class RenderSliverPinnedHeader extends RenderSliverSingleBoxAdapter { RenderSliverPinnedHeader({ required Color? backgroundColor, }) : _backgroundColor = backgroundColor; Color? _backgroundColor; set backgroundColor(Color? value) { if (_backgroundColor == value) return; _backgroundColor = value; if (_isPinned) markNeedsPaint(); } bool _isPinned = false; @override void performLayout() { final constraints = this.constraints; child!.layout(constraints.asBoxConstraints(), parentUsesSize: true); final double childExtent = child!.size.height; final double effectiveRemainingPaintExtent = math.max( 0, constraints.remainingPaintExtent - constraints.overlap, ); final double layoutExtent = clampDouble( childExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent, ); _isPinned = constraints.overlap > 0.0 || constraints.scrollOffset > 0.0; geometry = SliverGeometry( scrollExtent: childExtent, paintOrigin: constraints.overlap, paintExtent: math.min(childExtent, effectiveRemainingPaintExtent), layoutExtent: layoutExtent, maxPaintExtent: childExtent, maxScrollObstructionExtent: childExtent, cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent, hasVisualOverflow: false, ); } @override void paint(PaintingContext context, Offset offset) { if (child != null && geometry!.visible) { if (_isPinned && _backgroundColor != null) { final size = child!.size; context.canvas.drawRect( Rect.fromLTWH( offset.dx, offset.dy - 2, size.width, size.height + 2, ), Paint()..color = _backgroundColor!, ); } context.paintChild(child!, offset); } } @override double childMainAxisPosition(RenderBox child) => 0.0; @override bool hitTestSelf({ required double mainAxisPosition, required double crossAxisPosition, }) => true; } ================================================ FILE: lib/common/widgets/sliver_wrap.dart ================================================ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; class SliverFixedWrap extends SliverMultiBoxAdaptorWidget { final double mainAxisExtent; final double spacing; final double runSpacing; const SliverFixedWrap({ super.key, required super.delegate, required this.mainAxisExtent, this.spacing = 0, this.runSpacing = 0, }); @override SliverWrapElement createElement() => SliverWrapElement(this, replaceMovedChildren: true); @override RenderSliverFixedWrap createRenderObject(BuildContext context) { return RenderSliverFixedWrap( childManager: context as SliverWrapElement, mainAxisExtent: mainAxisExtent, spacing: spacing, runSpacing: runSpacing, ); } @override void updateRenderObject( BuildContext context, RenderSliverFixedWrap renderObject, ) { renderObject ..mainAxisExtent = mainAxisExtent ..spacing = spacing ..runSpacing = runSpacing; } } class SliverWrapParentData extends SliverMultiBoxAdaptorParentData { double crossAxisOffset = 0.0; @override String toString() => 'crossAxisOffset=$crossAxisOffset; ${super.toString()}'; } class _Row { final int startIndex; final int endIndex; final List childWidths; _Row({ required this.startIndex, required this.endIndex, required this.childWidths, }); } class RenderSliverFixedWrap extends RenderSliverMultiBoxAdaptor { RenderSliverFixedWrap({ required super.childManager, required double mainAxisExtent, double spacing = 0.0, double runSpacing = 0.0, }) : _mainAxisExtent = mainAxisExtent, _spacing = spacing, _runSpacing = runSpacing { assert(mainAxisExtent > 0.0 && mainAxisExtent.isFinite); } double _mainAxisExtent; double get mainAxisExtent => _mainAxisExtent; set mainAxisExtent(double value) { if (_mainAxisExtent == value) return; _mainAxisExtent = value; markRowsDirty(); markNeedsLayout(); } double _spacing; double get spacing => _spacing; set spacing(double value) { if (_spacing == value) return; _spacing = value; markRowsDirty(); markNeedsLayout(); } double _runSpacing; double get runSpacing => _runSpacing; set runSpacing(double value) { if (_runSpacing == value) return; _runSpacing = value; markNeedsLayout(); } final List<_Row> _rows = []; void markRowsDirty() { _rows.clear(); } @override void setupParentData(RenderObject child) { if (child.parentData is! SliverWrapParentData) { child.parentData = SliverWrapParentData(); } } @override double childCrossAxisPosition(RenderBox child) { return (child.parentData as SliverWrapParentData).crossAxisOffset; } double _childCrossExtent(RenderBox child) { assert(child.hasSize); return switch (constraints.axis) { Axis.horizontal => child.size.height, Axis.vertical => child.size.width, }; } RenderBox _getOrCreateChildAtIndex( int index, BoxConstraints constraints, RenderBox? child, ) { assert(firstChild != null); if (index < indexOf(firstChild!)) { do { child = insertAndLayoutLeadingChild(constraints, parentUsesSize: true); assert(child != null); } while (indexOf(child!) > index); assert(indexOf(child) == index); return child; } else if (index > indexOf(lastChild!)) { do { child = insertAndLayoutChild( constraints, after: lastChild, parentUsesSize: true, ); assert(child != null); } while (indexOf(child!) < index); assert(indexOf(child) == index); return child; } else { child = firstChild; while (indexOf(child!) < index) { child = childAfter(child); } if (indexOf(child) == index) { child.layout(constraints, parentUsesSize: true); return child; } throw RangeError.value(index, 'index', 'Value not included in children'); } } bool _buildNextRow(int start, BoxConstraints constraints) { final int childCount = childManager.childCount; if (start >= childCount) { return false; } final crossAxisExtent = this.constraints.crossAxisExtent; final List widths = []; int idx = start; RenderBox? child; for (var totalWidth = -_spacing; idx < childCount; idx++) { child = _getOrCreateChildAtIndex(idx, constraints, child); final childWidth = _childCrossExtent(child); totalWidth += childWidth + _spacing; if (totalWidth <= crossAxisExtent) { widths.add(childWidth); } else { break; } } _rows.add(_Row(startIndex: start, endIndex: idx - 1, childWidths: widths)); return true; } @override void performLayout() { childManager ..didStartLayout() ..setDidUnderflow(false); final constraints = this.constraints; final childCount = childManager.childCount; final rowHeight = _mainAxisExtent + _runSpacing; final scrollOffset = constraints.scrollOffset; final firstCacheOffset = scrollOffset + constraints.cacheOrigin; final lastCacheOffset = scrollOffset + constraints.remainingCacheExtent; final firstNeededRow = math.max(0, firstCacheOffset ~/ rowHeight); final lastNeededRow = math.max(0, lastCacheOffset ~/ rowHeight); final childConstraints = constraints.toFixedConstraints(_mainAxisExtent); if (firstChild == null) { if (!addInitialChild()) { geometry = SliverGeometry.zero; childManager.didFinishLayout(); return; } firstChild!.layout(childConstraints, parentUsesSize: true); } while (_rows.length <= lastNeededRow) { final int startIndex = _rows.isEmpty ? 0 : _rows.last.endIndex + 1; if (!_buildNextRow(startIndex, childConstraints)) { break; } } assert(firstNeededRow >= 0); final int firstKeptRow = firstNeededRow.clamp(0, _rows.length - 1); final int lastKeptRow = lastNeededRow.clamp(0, _rows.length - 1); final int firstKeptIndex = _rows[firstKeptRow].startIndex; final int lastKeptIndex = _rows[lastKeptRow].endIndex; collectGarbage( calculateLeadingGarbage(firstIndex: firstKeptIndex), calculateTrailingGarbage(lastIndex: lastKeptIndex), ); RenderBox? child; for (var r = firstKeptRow; r <= lastKeptRow; r++) { final row = _rows[r]; final rowStartOffset = r * rowHeight; double crossOffset = 0.0; for (var i = row.startIndex; i <= row.endIndex; i++) { child = _getOrCreateChildAtIndex(i, childConstraints, child); (child.parentData as SliverWrapParentData) ..layoutOffset = rowStartOffset ..crossAxisOffset = crossOffset; crossOffset += row.childWidths[i - row.startIndex] + _spacing; } } final endOffset = _rows.last.endIndex == childCount - 1 ? (_rows.length * rowHeight) : (_rows.last.startIndex + 1) * rowHeight; final double estimatedMaxScrollOffset; if (_rows.length <= lastNeededRow || childCount == 0) { estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset( constraints, firstIndex: firstKeptIndex, lastIndex: lastKeptIndex, leadingScrollOffset: firstKeptRow * rowHeight, trailingScrollOffset: endOffset, ); } else { estimatedMaxScrollOffset = _rows.length * rowHeight; } final double paintExtent = calculatePaintOffset( constraints, from: firstKeptRow * rowHeight, to: endOffset, ); final double cacheExtent = calculateCacheOffset( constraints, from: firstCacheOffset, to: lastCacheOffset, ); geometry = SliverGeometry( scrollExtent: estimatedMaxScrollOffset, paintExtent: paintExtent, cacheExtent: cacheExtent, maxPaintExtent: estimatedMaxScrollOffset, hasVisualOverflow: endOffset > constraints.scrollOffset + constraints.remainingPaintExtent, ); if (estimatedMaxScrollOffset <= endOffset) { childManager.setDidUnderflow(true); } childManager.didFinishLayout(); } @override void dispose() { markRowsDirty(); super.dispose(); } } class SliverWrapElement extends SliverMultiBoxAdaptorElement { SliverWrapElement(SliverFixedWrap super.widget, {super.replaceMovedChildren}); @override void performRebuild() { (renderObject as RenderSliverFixedWrap).markRowsDirty(); super.performRebuild(); } } extension on SliverConstraints { BoxConstraints toFixedConstraints(double mainAxisExtent) { switch (axis) { case Axis.horizontal: return BoxConstraints( minHeight: 0, maxHeight: crossAxisExtent, minWidth: mainAxisExtent, maxWidth: mainAxisExtent, ); case Axis.vertical: return BoxConstraints( minWidth: 0, maxWidth: crossAxisExtent, minHeight: mainAxisExtent, maxHeight: mainAxisExtent, ); } } } ================================================ FILE: lib/common/widgets/stat/stat.dart ================================================ import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:flutter/material.dart'; class StatWidget extends StatelessWidget { final StatType type; final dynamic value; final Color? color; final double iconSize; const StatWidget({ super.key, required this.type, required this.value, this.color, this.iconSize = 13, }); @override Widget build(BuildContext context) { Color color = this.color ?? Theme.of(context).colorScheme.outline.withValues(alpha: 0.8); return Row( spacing: 2, mainAxisSize: MainAxisSize.min, children: [ Icon( type.iconData, semanticLabel: type.label, size: iconSize, color: color, ), Text( NumUtils.numFormat(value), style: TextStyle(fontSize: 12, color: color), ), ], ); } } ================================================ FILE: lib/common/widgets/stateful_builder.dart ================================================ import 'package:flutter/material.dart'; class StatefulBuilder extends StatefulWidget { const StatefulBuilder({ super.key, this.onInit, this.onDispose, required this.builder, }); final VoidCallback? onInit; final VoidCallback? onDispose; final StatefulWidgetBuilder builder; @override State createState() => _StatefulBuilderState(); } class _StatefulBuilderState extends State { @override void initState() { super.initState(); widget.onInit?.call(); } @override void dispose() { widget.onDispose?.call(); super.dispose(); } @override Widget build(BuildContext context) => widget.builder(context, setState); } ================================================ FILE: lib/common/widgets/time_picker.dart ================================================ import 'package:flutter/material.dart' as material; Future showTimePicker({ required material.BuildContext context, required material.TimeOfDay initialTime, }) => material.showTimePicker( context: context, initialTime: initialTime, builder: (context, child) => material.DialogTheme( data: material.DialogTheme.of( context, ).copyWith(constraints: const material.BoxConstraints(minWidth: 280)), child: child, ), ); ================================================ FILE: lib/common/widgets/video_card/video_card_h.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/common/widgets/video_popup_menu.dart'; import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models/model_hot_video_item.dart'; import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart' hide LayoutBuilder; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; // 视频卡片 - 水平布局 class VideoCardH extends StatelessWidget { const VideoCardH({ super.key, required this.videoItem, this.onTap, this.onViewLater, this.onRemove, }); final BaseVideoItemModel videoItem; final VoidCallback? onTap; final ValueChanged? onViewLater; final VoidCallback? onRemove; @override Widget build(BuildContext context) { String type = 'video'; String? badge; if (videoItem case final SearchVideoItemModel item) { final typeOrNull = item.type; if (typeOrNull != null && typeOrNull.isNotEmpty) { type = typeOrNull; if (type == 'ketang') { badge = '课堂'; } else if (type == 'live_room') { badge = '直播'; } } if (item.isUnionVideo == 1) { badge = '合作'; } } else if (videoItem case final HotVideoItemModel item) { if (item.isCharging == true) { badge = '充电专属'; } else if (item.isCooperation == 1) { badge = '合作'; } else { badge = item.pgcLabel; } } void onLongPress() => imageSaveDialog( bvid: videoItem.bvid, title: videoItem.title, cover: videoItem.cover, ); final colorScheme = ColorScheme.of(context); return Material( type: MaterialType.transparency, child: Stack( clipBehavior: Clip.none, children: [ InkWell( onLongPress: onLongPress, onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress, onTap: onTap ?? () async { if (type == 'ketang') { PageUtils.viewPugv(seasonId: videoItem.aid); return; } else if (type == 'live_room') { if (videoItem case final SearchVideoItemModel item) { int? roomId = item.id; if (roomId != null) { PageUtils.toLiveRoom(roomId); } } else { SmartDialog.showToast( 'err: live_room : ${videoItem.runtimeType}', ); } return; } if (videoItem case final HotVideoItemModel item) { if (item.redirectUrl?.isNotEmpty == true && PageUtils.viewPgcFromUri(item.redirectUrl!)) { return; } } try { final int? cid = videoItem.cid ?? await SearchHttp.ab2c( aid: videoItem.aid, bvid: videoItem.bvid, ); if (cid != null) { PageUtils.toVideoPage( bvid: videoItem.bvid, cid: cid, cover: videoItem.cover, title: videoItem.title, ); } } catch (err) { SmartDialog.showToast(err.toString()); } }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace, vertical: 5, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: StyleString.aspectRatio, child: LayoutBuilder( builder: (context, boxConstraints) { final double maxWidth = boxConstraints.maxWidth; final double maxHeight = boxConstraints.maxHeight; num? progress; if (videoItem case final HotVideoItemModel item) { progress = item.progress; } return Stack( clipBehavior: Clip.none, children: [ NetworkImgLayer( src: videoItem.cover, width: maxWidth, height: maxHeight, ), if (badge != null) PBadge( text: badge, top: 6.0, right: 6.0, type: switch (badge) { '充电专属' => PBadgeType.error, _ => PBadgeType.primary, }, ), if (progress != null && progress != 0) ...[ PBadge( text: progress == -1 ? '已看完' : '${DurationUtils.formatDuration(progress)}/${DurationUtils.formatDuration(videoItem.duration)}', right: 6, bottom: 8, type: PBadgeType.gray, ), Positioned( left: 0, bottom: 0, right: 0, child: VideoProgressIndicator( color: colorScheme.primary, backgroundColor: colorScheme.secondaryContainer, progress: progress == -1 ? 1 : progress / videoItem.duration, ), ), ] else if (videoItem.duration > 0) PBadge( text: DurationUtils.formatDuration( videoItem.duration, ), right: 6.0, bottom: 6.0, type: PBadgeType.gray, ), ], ); }, ), ), const SizedBox(width: 10), content(context), ], ), ), ), Positioned( bottom: 0, right: 12, width: 29, height: 29, child: VideoPopupMenu( iconSize: 17, videoItem: videoItem, onRemove: onRemove, ), ), ], ), ); } Widget content(BuildContext context) { final theme = Theme.of(context); String pubdate = DateFormatUtils.dateFormat(videoItem.pubdate!); if (pubdate != '') pubdate += ' '; return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (videoItem case final SearchVideoItemModel item) ...[ if (item.titleList?.isNotEmpty == true) Expanded( child: Text.rich( overflow: TextOverflow.ellipsis, maxLines: 2, TextSpan( children: item.titleList! .map( (e) => TextSpan( text: e.text, style: TextStyle( fontSize: theme.textTheme.bodyMedium!.fontSize, height: 1.42, letterSpacing: 0.3, color: e.isEm ? theme.colorScheme.primary : theme.colorScheme.onSurface, ), ), ) .toList(), ), ), ), ] else Expanded( child: Text( videoItem.title, textAlign: TextAlign.start, style: TextStyle( fontSize: theme.textTheme.bodyMedium!.fontSize, height: 1.42, letterSpacing: 0.3, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Text( "$pubdate${videoItem.owner.name}", maxLines: 1, style: TextStyle( fontSize: 12, height: 1, color: theme.colorScheme.outline, overflow: TextOverflow.clip, ), ), const SizedBox(height: 3), Row( spacing: 8, children: [ StatWidget( type: StatType.play, value: videoItem.stat.view, ), StatWidget( type: StatType.danmaku, value: videoItem.stat.danmu, ), ], ), ], ), ); } } ================================================ FILE: lib/common/widgets/video_card/video_card_v.dart ================================================ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/common/widgets/video_popup_menu.dart'; import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models/model_rec_video_item.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide LayoutBuilder; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:intl/intl.dart'; // 视频卡片 - 垂直布局 class VideoCardV extends StatelessWidget { final BaseRecVideoItemModel videoItem; final VoidCallback? onRemove; const VideoCardV({ super.key, required this.videoItem, this.onRemove, }); Future onPushDetail(String heroTag) async { String? goto = videoItem.goto; switch (goto) { case 'bangumi': PageUtils.viewPgc(epId: videoItem.param!); break; case 'av': String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid!); int? cid = videoItem.cid ?? await SearchHttp.ab2c(aid: videoItem.aid, bvid: bvid); if (cid != null) { PageUtils.toVideoPage( aid: videoItem.aid, bvid: bvid, cid: cid, cover: videoItem.cover, title: videoItem.title, ); } break; // 动态 case 'picture': try { PiliScheme.routePushFromUrl(videoItem.uri!); } catch (err) { SmartDialog.showToast(err.toString()); } break; default: if (videoItem.uri?.isNotEmpty == true) { PiliScheme.routePushFromUrl(videoItem.uri!); } } } @override Widget build(BuildContext context) { void onLongPress() => imageSaveDialog( title: videoItem.title, cover: videoItem.cover, bvid: videoItem.bvid, ); return Stack( clipBehavior: Clip.none, children: [ Card( clipBehavior: Clip.hardEdge, child: InkWell( onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.aid)), onLongPress: onLongPress, onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: StyleString.aspectRatio, child: LayoutBuilder( builder: (context, boxConstraints) { double maxWidth = boxConstraints.maxWidth; double maxHeight = boxConstraints.maxHeight; return Stack( clipBehavior: Clip.none, children: [ NetworkImgLayer( src: videoItem.cover, width: maxWidth, height: maxHeight, type: .emote, ), if (videoItem.duration > 0) PBadge( bottom: 6, right: 7, size: PBadgeSize.small, type: PBadgeType.gray, text: DurationUtils.formatDuration( videoItem.duration, ), ), ], ); }, ), ), content(context), ], ), ), ), if (videoItem.goto == 'av') Positioned( right: -5, bottom: -2, width: 29, height: 29, child: VideoPopupMenu( iconSize: 17, videoItem: videoItem, onRemove: onRemove, ), ), ], ); } Widget content(BuildContext context) { final theme = Theme.of(context); return Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(6, 5, 6, 5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( "${videoItem.title}\n", maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( height: 1.38, ), ), ), videoStat(context, theme), Row( spacing: 2, children: [ if (videoItem.goto == 'bangumi') PBadge( text: videoItem.pgcBadge, isStack: false, size: PBadgeSize.small, type: PBadgeType.line_primary, fontSize: 9, ), if (videoItem.rcmdReason != null) PBadge( text: videoItem.rcmdReason, isStack: false, size: PBadgeSize.small, type: PBadgeType.secondary, ), if (videoItem.goto == 'picture') const PBadge( text: '动态', isStack: false, size: PBadgeSize.small, type: PBadgeType.line_primary, fontSize: 9, ), if (videoItem.isFollowed) const PBadge( text: '已关注', isStack: false, size: PBadgeSize.small, type: PBadgeType.secondary, ), Expanded( flex: 1, child: Text( videoItem.owner.name.toString(), maxLines: 1, overflow: TextOverflow.clip, semanticsLabel: 'UP:${videoItem.owner.name}', style: TextStyle( height: 1.5, fontSize: theme.textTheme.labelMedium!.fontSize, color: theme.colorScheme.outline, ), ), ), if (videoItem.goto == 'av') const SizedBox(width: 10), ], ), ], ), ), ); } static final shortFormat = DateFormat('M-d'); static final longFormat = DateFormat('yy-M-d'); Widget videoStat(BuildContext context, ThemeData theme) { return Row( children: [ StatWidget( type: StatType.play, value: videoItem.stat.view, ), if (videoItem.goto != 'picture') ...[ const SizedBox(width: 4), StatWidget( type: StatType.danmaku, value: videoItem.stat.danmu, ), ], if (videoItem is RecVideoItemModel) ...[ const Spacer(), Text.rich( maxLines: 1, TextSpan( style: TextStyle( fontSize: theme.textTheme.labelSmall!.fontSize, color: theme.colorScheme.outline.withValues(alpha: 0.8), ), text: DateFormatUtils.dateFormat( videoItem.pubdate, short: shortFormat, long: longFormat, ), ), ), const SizedBox(width: 2), ], // deprecated // else if (videoItem is RecVideoItemAppModel && // videoItem.desc != null && // videoItem.desc!.contains(' · ')) ...[ // const Spacer(), // Text.rich( // maxLines: 1, // TextSpan( // style: TextStyle( // fontSize: theme.textTheme.labelSmall!.fontSize, // color: theme.colorScheme.outline.withValues(alpha: 0.8), // ), // text: Utils.shortenChineseDateString( // videoItem.desc!.split(' · ').last)), // ), // const SizedBox(width: 2), // ] ], ); } } ================================================ FILE: lib/common/widgets/video_popup_menu.dart ================================================ import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/home/rcmd/result.dart'; import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/pages/video/ai_conclusion/view.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class _VideoCustomAction { final String title; final Widget icon; final VoidCallback onTap; const _VideoCustomAction(this.title, this.icon, this.onTap); } class VideoPopupMenu extends StatelessWidget { final double? iconSize; final double menuItemHeight; final BaseSimpleVideoItemModel videoItem; final VoidCallback? onRemove; const VideoPopupMenu({ super.key, required this.iconSize, required this.videoItem, this.onRemove, this.menuItemHeight = 45, }); @override Widget build(BuildContext context) { return PopupMenuButton( padding: EdgeInsets.zero, icon: Icon( Icons.more_vert_outlined, color: Theme.of(context).colorScheme.outline, size: iconSize, ), position: PopupMenuPosition.under, itemBuilder: (context) => [ if (videoItem.bvid?.isNotEmpty == true) ...[ _VideoCustomAction( videoItem.bvid!, const Stack( clipBehavior: Clip.none, children: [ Icon(MdiIcons.identifier, size: 16), Icon(MdiIcons.circleOutline, size: 16), ], ), () => Utils.copyText(videoItem.bvid!), ), _VideoCustomAction( '稍后再看', const Icon(MdiIcons.clockTimeEightOutline, size: 16), () => UserHttp.toViewLater(bvid: videoItem.bvid), ), if (videoItem.cid != null && Pref.enableAi) _VideoCustomAction( 'AI总结', const Stack( alignment: Alignment.center, clipBehavior: Clip.none, children: [ Icon(Icons.circle_outlined, size: 16), ExcludeSemantics( child: Text( 'AI', style: TextStyle( fontSize: 10, height: 1, fontWeight: FontWeight.w700, ), strutStyle: StrutStyle( fontSize: 10, height: 1, leading: 0, fontWeight: FontWeight.w700, ), textScaler: TextScaler.noScaling, ), ), ], ), () async { final res = await UgcIntroController.getAiConclusion( videoItem.bvid!, videoItem.cid!, videoItem.owner.mid, ); if (res != null && context.mounted) { showDialog( context: context, builder: (context) => Dialog( child: Padding( padding: const .symmetric(vertical: 14), child: AiConclusionPanel.buildContent( context, Theme.of(context), res, tap: false, ), ), ), ); } }, ), ], if (videoItem is! SpaceArchiveItem) ...[ _VideoCustomAction( '访问:${videoItem.owner.name}', const Icon(MdiIcons.accountCircleOutline, size: 16), () => Get.toNamed('/member?mid=${videoItem.owner.mid}'), ), _VideoCustomAction( '不感兴趣', const Icon(MdiIcons.thumbDownOutline, size: 16), () { String? accessKey = Accounts.get( AccountType.recommend, ).accessKey; if (accessKey == null || accessKey == "") { SmartDialog.showToast("请退出账号后重新登录"); return; } if (videoItem case final RecVideoItemAppModel item) { ThreePoint? tp = item.threePoint; if (tp == null) { SmartDialog.showToast("未能获取threePoint"); return; } if (tp.dislikeReasons == null && tp.feedbacks == null) { SmartDialog.showToast( "未能获取dislikeReasons或feedbacks", ); return; } Widget actionButton(Reason? r, Reason? f) { return SearchText( text: r?.name ?? f?.name ?? '未知', onTap: (_) async { Get.back(); SmartDialog.showLoading(msg: '正在提交'); final res = await VideoHttp.feedDislike( reasonId: r?.id, feedbackId: f?.id, id: item.param!, goto: item.goto!, ); SmartDialog.dismiss(); if (res.isSuccess) { SmartDialog.showToast( r?.toast ?? f!.toast!, ); onRemove?.call(); } else { res.toast(); } }, ); } showDialog( context: context, builder: (context) { return AlertDialog( content: SingleChildScrollView( child: Column( crossAxisAlignment: .start, children: [ if (tp.dislikeReasons != null) ...[ const Text('我不想看'), const SizedBox(height: 5), Wrap( spacing: 8.0, runSpacing: 8.0, children: tp.dislikeReasons!.map(( item, ) { return actionButton(item, null); }).toList(), ), ], if (tp.feedbacks != null) ...[ const SizedBox(height: 5), const Text('反馈'), const SizedBox(height: 5), Wrap( spacing: 8.0, runSpacing: 8.0, children: tp.feedbacks!.map((item) { return actionButton(null, item); }).toList(), ), ], const Divider(), Center( child: FilledButton.tonal( onPressed: () async { SmartDialog.showLoading( msg: '正在提交', ); final res = await VideoHttp.feedDislikeCancel( id: item.param!, goto: item.goto!, ); SmartDialog.dismiss(); SmartDialog.showToast( res.isSuccess ? "成功" : res.toString(), ); Get.back(); }, style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), child: const Text("撤销"), ), ), ], ), ), ); }, ); } else { showDialog( context: context, builder: (context) => AlertDialog( content: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 5), const Text("web端暂不支持精细选择"), const SizedBox(height: 5), Wrap( spacing: 5.0, runSpacing: 2.0, children: [ FilledButton.tonal( onPressed: () async { Get.back(); SmartDialog.showLoading( msg: '正在提交', ); final res = await VideoHttp.dislikeVideo( bvid: videoItem.bvid!, type: true, ); SmartDialog.dismiss(); if (res.isSuccess) { SmartDialog.showToast('点踩成功'); onRemove?.call(); } else { res.toast(); } }, style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), child: const Text("点踩"), ), FilledButton.tonal( onPressed: () async { Get.back(); SmartDialog.showLoading( msg: '正在提交', ); final res = await VideoHttp.dislikeVideo( bvid: videoItem.bvid!, type: false, ); SmartDialog.dismiss(); SmartDialog.showToast( res.isSuccess ? '取消踩' : res.toString(), ); }, style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), child: const Text("撤销"), ), ], ), ], ), ), ), ); } }, ), _VideoCustomAction( '拉黑:${videoItem.owner.name}', const Icon(MdiIcons.cancel, size: 16), () => showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('提示'), content: Text( '确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除', ), actions: [ TextButton( onPressed: Get.back, child: Text( '点错了', style: TextStyle( color: Theme.of( context, ).colorScheme.outline, ), ), ), TextButton( onPressed: () async { Get.back(); final res = await VideoHttp.relationMod( mid: videoItem.owner.mid!, act: 5, reSrc: 11, ); if (res.isSuccess) { onRemove?.call(); } else { res.toast(); } }, child: const Text('确认'), ), ], ); }, ), ), ], _VideoCustomAction( "${MineController.anonymity.value ? '退出' : '进入'}无痕模式", MineController.anonymity.value ? const Icon(MdiIcons.incognitoOff, size: 16) : const Icon(MdiIcons.incognito, size: 16), MineController.onChangeAnonymity, ), ] .map( (e) => PopupMenuItem( height: menuItemHeight, onTap: e.onTap, child: Row( children: [ e.icon, const SizedBox(width: 6), Text(e.title, style: const TextStyle(fontSize: 13)), ], ), ), ) .toList(), ); } } ================================================ FILE: lib/common/widgets/view_safe_area.dart ================================================ import 'package:flutter/material.dart'; class ViewSafeArea extends StatelessWidget { const ViewSafeArea({ super.key, this.top = false, this.left = true, this.right = true, required this.child, }); final bool top; final bool left; final bool right; final Widget child; @override Widget build(BuildContext context) { EdgeInsets padding = MediaQuery.viewPaddingOf(context); return Padding( padding: EdgeInsets.only( top: top ? padding.top : 0.0, left: left ? padding.left : 0.0, right: right ? padding.right : 0.0, ), child: child, ); } } ================================================ FILE: lib/common/widgets/view_sliver_safe_area.dart ================================================ import 'package:flutter/material.dart'; class ViewSliverSafeArea extends StatelessWidget { const ViewSliverSafeArea({ super.key, required this.sliver, }); final Widget sliver; @override Widget build(BuildContext context) { EdgeInsets padding = MediaQuery.viewPaddingOf(context); return SliverPadding( padding: EdgeInsets.only( left: padding.left, right: padding.right, bottom: padding.bottom + 100, ), sliver: sliver, ); } } ================================================ FILE: lib/grpc/audio.dart ================================================ import 'package:PiliPlus/grpc/bilibili/app/archive/middleware/v1.pb.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'; import 'package:PiliPlus/grpc/bilibili/pagination.pb.dart'; import 'package:PiliPlus/grpc/grpc_req.dart'; import 'package:PiliPlus/grpc/url.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:fixnum/fixnum.dart'; abstract final class AudioGrpc { static Future> audioPlayUrl({ required Int64 oid, required List subId, required int itemType, int qn = 80, int fnval = 4048, }) { return GrpcReq.request( GrpcUrl.audioPlayUrl, PlayURLReq( item: PlayItem( oid: oid, subId: subId, itemType: itemType, ), playerArgs: PlayerArgs( qn: Int64(qn), fnval: Int64(fnval), forceHost: Int64(2), voiceBalance: Int64(1), ), ), PlayURLResp.fromBuffer, ); } static Future> audioPlayList({ PlaylistSource? from, required Int64 id, Int64? oid, List? subId, int? itemType, PageOption? pageOpt, Int64? extraId, String? next, int qn = 80, int fnval = 4048, ListOrder order = ListOrder.ORDER_NORMAL, }) { return GrpcReq.request( GrpcUrl.audioPlayList, PlaylistReq( from: from, id: id, anchor: PlayItem( oid: oid, subId: subId, itemType: itemType, ), pageOpt: pageOpt, playerArgs: PlayerArgs( qn: Int64(qn), fnval: Int64(fnval), forceHost: Int64(2), voiceBalance: Int64(1), ), extraId: extraId, sortOpt: SortOption(order: order), pagination: Pagination(pageSize: 20, next: next), ), PlaylistResp.fromBuffer, ); } static Future> audioThumbUp({ required Int64 oid, required List subId, required int itemType, required ThumbUpReq_ThumbType type, }) { return GrpcReq.request( GrpcUrl.audioThumbUp, ThumbUpReq( item: PlayItem( oid: oid, itemType: itemType, subId: subId, ), action: type, ), ThumbUpResp.fromBuffer, ); } static Future> audioTripleLike({ required Int64 oid, required List subId, required int itemType, }) { return GrpcReq.request( GrpcUrl.audioTripleLike, TripleLikeReq( item: PlayItem( oid: oid, subId: subId, itemType: itemType, ), ), TripleLikeResp.fromBuffer, ); } static Future> audioCoinAdd({ required Int64 oid, required List subId, required int itemType, required int num, bool thumbUp = false, }) { return GrpcReq.request( GrpcUrl.audioCoinAdd, CoinAddReq( item: PlayItem( oid: oid, subId: subId, itemType: itemType, ), num: num, thumbUp: thumbUp, ), CoinAddResp.fromBuffer, ); } } ================================================ FILE: lib/grpc/bilibili/account/service/v1.pb.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/account/service/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'v1.pbenum.dart'; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; export 'v1.pbenum.dart'; class Color extends $pb.GeneratedMessage { factory Color({ $core.String? colorDay, $core.String? colorNight, }) { final result = create(); if (colorDay != null) result.colorDay = colorDay; if (colorNight != null) result.colorNight = colorNight; return result; } Color._(); factory Color.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Color.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Color', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.account.service.v1'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'colorDay') ..aOS(2, _omitFieldNames ? '' : 'colorNight') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Color clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Color copyWith(void Function(Color) updates) => super.copyWith((message) => updates(message as Color)) as Color; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Color create() => Color._(); @$core.override Color createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Color getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Color? _defaultInstance; @$pb.TagNumber(1) $core.String get colorDay => $_getSZ(0); @$pb.TagNumber(1) set colorDay($core.String value) => $_setString(0, value); @$pb.TagNumber(1) $core.bool hasColorDay() => $_has(0); @$pb.TagNumber(1) void clearColorDay() => $_clearField(1); @$pb.TagNumber(2) $core.String get colorNight => $_getSZ(1); @$pb.TagNumber(2) set colorNight($core.String value) => $_setString(1, value); @$pb.TagNumber(2) $core.bool hasColorNight() => $_has(1); @$pb.TagNumber(2) void clearColorNight() => $_clearField(2); } class ColorsInfo extends $pb.GeneratedMessage { factory ColorsInfo({ $core.Iterable<$fixnum.Int64>? colorIds, $core.Iterable? color, }) { final result = create(); if (colorIds != null) result.colorIds.addAll(colorIds); if (color != null) result.color.addAll(color); return result; } ColorsInfo._(); factory ColorsInfo.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory ColorsInfo.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'ColorsInfo', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.account.service.v1'), createEmptyInstance: create) ..p<$fixnum.Int64>(1, _omitFieldNames ? '' : 'colorIds', $pb.PbFieldType.K6) ..pPM(2, _omitFieldNames ? '' : 'color', subBuilder: Color.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') ColorsInfo clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') ColorsInfo copyWith(void Function(ColorsInfo) updates) => super.copyWith((message) => updates(message as ColorsInfo)) as ColorsInfo; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ColorsInfo create() => ColorsInfo._(); @$core.override ColorsInfo createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static ColorsInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ColorsInfo? _defaultInstance; @$pb.TagNumber(1) $pb.PbList<$fixnum.Int64> get colorIds => $_getList(0); @$pb.TagNumber(2) $pb.PbList get color => $_getList(1); } class NameRender extends $pb.GeneratedMessage { factory NameRender({ RenderSchemeEnum? renderScheme, ColorsInfo? colorsInfo, }) { final result = create(); if (renderScheme != null) result.renderScheme = renderScheme; if (colorsInfo != null) result.colorsInfo = colorsInfo; return result; } NameRender._(); factory NameRender.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory NameRender.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'NameRender', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.account.service.v1'), createEmptyInstance: create) ..aE(1, _omitFieldNames ? '' : 'renderScheme', enumValues: RenderSchemeEnum.values) ..aOM(2, _omitFieldNames ? '' : 'colorsInfo', subBuilder: ColorsInfo.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') NameRender clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') NameRender copyWith(void Function(NameRender) updates) => super.copyWith((message) => updates(message as NameRender)) as NameRender; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static NameRender create() => NameRender._(); @$core.override NameRender createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static NameRender getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static NameRender? _defaultInstance; @$pb.TagNumber(1) RenderSchemeEnum get renderScheme => $_getN(0); @$pb.TagNumber(1) set renderScheme(RenderSchemeEnum value) => $_setField(1, value); @$pb.TagNumber(1) $core.bool hasRenderScheme() => $_has(0); @$pb.TagNumber(1) void clearRenderScheme() => $_clearField(1); @$pb.TagNumber(2) ColorsInfo get colorsInfo => $_getN(1); @$pb.TagNumber(2) set colorsInfo(ColorsInfo value) => $_setField(2, value); @$pb.TagNumber(2) $core.bool hasColorsInfo() => $_has(1); @$pb.TagNumber(2) void clearColorsInfo() => $_clearField(2); @$pb.TagNumber(2) ColorsInfo ensureColorsInfo() => $_ensure(1); } const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); ================================================ FILE: lib/grpc/bilibili/account/service/v1.pbenum.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/account/service/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; class RenderSchemeEnum extends $pb.ProtobufEnum { static const RenderSchemeEnum Default = RenderSchemeEnum._(0, _omitEnumNames ? '' : 'Default'); static const RenderSchemeEnum Colorful = RenderSchemeEnum._(1, _omitEnumNames ? '' : 'Colorful'); static const $core.List values = [ Default, Colorful, ]; static final $core.List _byValue = $pb.ProtobufEnum.$_initByValueList(values, 1); static RenderSchemeEnum? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; const RenderSchemeEnum._(super.value, super.name); } const $core.bool _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); ================================================ FILE: lib/grpc/bilibili/account/service/v1.pbjson.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/account/service/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports // ignore_for_file: unused_import import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; @$core.Deprecated('Use renderSchemeEnumDescriptor instead') const RenderSchemeEnum$json = { '1': 'RenderSchemeEnum', '2': [ {'1': 'Default', '2': 0}, {'1': 'Colorful', '2': 1}, ], }; /// Descriptor for `RenderSchemeEnum`. Decode as a `google.protobuf.EnumDescriptorProto`. final $typed_data.Uint8List renderSchemeEnumDescriptor = $convert.base64Decode( 'ChBSZW5kZXJTY2hlbWVFbnVtEgsKB0RlZmF1bHQQABIMCghDb2xvcmZ1bBAB'); @$core.Deprecated('Use colorDescriptor instead') const Color$json = { '1': 'Color', '2': [ {'1': 'color_day', '3': 1, '4': 1, '5': 9, '10': 'colorDay'}, {'1': 'color_night', '3': 2, '4': 1, '5': 9, '10': 'colorNight'}, ], }; /// Descriptor for `Color`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List colorDescriptor = $convert.base64Decode( 'CgVDb2xvchIbCgljb2xvcl9kYXkYASABKAlSCGNvbG9yRGF5Eh8KC2NvbG9yX25pZ2h0GAIgAS' 'gJUgpjb2xvck5pZ2h0'); @$core.Deprecated('Use colorsInfoDescriptor instead') const ColorsInfo$json = { '1': 'ColorsInfo', '2': [ {'1': 'color_ids', '3': 1, '4': 3, '5': 3, '10': 'colorIds'}, { '1': 'color', '3': 2, '4': 3, '5': 11, '6': '.bilibili.account.service.v1.Color', '10': 'color' }, ], }; /// Descriptor for `ColorsInfo`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List colorsInfoDescriptor = $convert.base64Decode( 'CgpDb2xvcnNJbmZvEhsKCWNvbG9yX2lkcxgBIAMoA1IIY29sb3JJZHMSOAoFY29sb3IYAiADKA' 'syIi5iaWxpYmlsaS5hY2NvdW50LnNlcnZpY2UudjEuQ29sb3JSBWNvbG9y'); @$core.Deprecated('Use nameRenderDescriptor instead') const NameRender$json = { '1': 'NameRender', '2': [ { '1': 'render_scheme', '3': 1, '4': 1, '5': 14, '6': '.bilibili.account.service.v1.RenderSchemeEnum', '10': 'renderScheme' }, { '1': 'colors_info', '3': 2, '4': 1, '5': 11, '6': '.bilibili.account.service.v1.ColorsInfo', '10': 'colorsInfo' }, ], }; /// Descriptor for `NameRender`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List nameRenderDescriptor = $convert.base64Decode( 'CgpOYW1lUmVuZGVyElIKDXJlbmRlcl9zY2hlbWUYASABKA4yLS5iaWxpYmlsaS5hY2NvdW50Ln' 'NlcnZpY2UudjEuUmVuZGVyU2NoZW1lRW51bVIMcmVuZGVyU2NoZW1lEkgKC2NvbG9yc19pbmZv' 'GAIgASgLMicuYmlsaWJpbGkuYWNjb3VudC5zZXJ2aWNlLnYxLkNvbG9yc0luZm9SCmNvbG9yc0' 'luZm8='); ================================================ FILE: lib/grpc/bilibili/app/archive/middleware/v1.pb.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/archive/middleware/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'v1.pbenum.dart'; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; export 'v1.pbenum.dart'; class PlayerArgs extends $pb.GeneratedMessage { factory PlayerArgs({ $fixnum.Int64? qn, $fixnum.Int64? fnver, $fixnum.Int64? fnval, $fixnum.Int64? forceHost, $fixnum.Int64? voiceBalance, QnPolicy? qnPolicy, }) { final result = create(); if (qn != null) result.qn = qn; if (fnver != null) result.fnver = fnver; if (fnval != null) result.fnval = fnval; if (forceHost != null) result.forceHost = forceHost; if (voiceBalance != null) result.voiceBalance = voiceBalance; if (qnPolicy != null) result.qnPolicy = qnPolicy; return result; } PlayerArgs._(); factory PlayerArgs.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory PlayerArgs.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'PlayerArgs', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.middleware.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'qn') ..aInt64(2, _omitFieldNames ? '' : 'fnver') ..aInt64(3, _omitFieldNames ? '' : 'fnval') ..aInt64(4, _omitFieldNames ? '' : 'forceHost') ..aInt64(5, _omitFieldNames ? '' : 'voiceBalance') ..aE(6, _omitFieldNames ? '' : 'qnPolicy', enumValues: QnPolicy.values) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') PlayerArgs clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') PlayerArgs copyWith(void Function(PlayerArgs) updates) => super.copyWith((message) => updates(message as PlayerArgs)) as PlayerArgs; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static PlayerArgs create() => PlayerArgs._(); @$core.override PlayerArgs createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static PlayerArgs getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static PlayerArgs? _defaultInstance; @$pb.TagNumber(1) $fixnum.Int64 get qn => $_getI64(0); @$pb.TagNumber(1) set qn($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasQn() => $_has(0); @$pb.TagNumber(1) void clearQn() => $_clearField(1); @$pb.TagNumber(2) $fixnum.Int64 get fnver => $_getI64(1); @$pb.TagNumber(2) set fnver($fixnum.Int64 value) => $_setInt64(1, value); @$pb.TagNumber(2) $core.bool hasFnver() => $_has(1); @$pb.TagNumber(2) void clearFnver() => $_clearField(2); @$pb.TagNumber(3) $fixnum.Int64 get fnval => $_getI64(2); @$pb.TagNumber(3) set fnval($fixnum.Int64 value) => $_setInt64(2, value); @$pb.TagNumber(3) $core.bool hasFnval() => $_has(2); @$pb.TagNumber(3) void clearFnval() => $_clearField(3); @$pb.TagNumber(4) $fixnum.Int64 get forceHost => $_getI64(3); @$pb.TagNumber(4) set forceHost($fixnum.Int64 value) => $_setInt64(3, value); @$pb.TagNumber(4) $core.bool hasForceHost() => $_has(3); @$pb.TagNumber(4) void clearForceHost() => $_clearField(4); @$pb.TagNumber(5) $fixnum.Int64 get voiceBalance => $_getI64(4); @$pb.TagNumber(5) set voiceBalance($fixnum.Int64 value) => $_setInt64(4, value); @$pb.TagNumber(5) $core.bool hasVoiceBalance() => $_has(4); @$pb.TagNumber(5) void clearVoiceBalance() => $_clearField(5); @$pb.TagNumber(6) QnPolicy get qnPolicy => $_getN(5); @$pb.TagNumber(6) set qnPolicy(QnPolicy value) => $_setField(6, value); @$pb.TagNumber(6) $core.bool hasQnPolicy() => $_has(5); @$pb.TagNumber(6) void clearQnPolicy() => $_clearField(6); } const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); ================================================ FILE: lib/grpc/bilibili/app/archive/middleware/v1.pbenum.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/archive/middleware/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; class QnPolicy extends $pb.ProtobufEnum { static const QnPolicy QN_POLICY_DEFAULT = QnPolicy._(0, _omitEnumNames ? '' : 'QN_POLICY_DEFAULT'); static const QnPolicy QN_POLICY_AUTO_QN_ENABLE = QnPolicy._(1, _omitEnumNames ? '' : 'QN_POLICY_AUTO_QN_ENABLE'); static const $core.List values = [ QN_POLICY_DEFAULT, QN_POLICY_AUTO_QN_ENABLE, ]; static final $core.List _byValue = $pb.ProtobufEnum.$_initByValueList(values, 1); static QnPolicy? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; const QnPolicy._(super.value, super.name); } const $core.bool _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); ================================================ FILE: lib/grpc/bilibili/app/archive/middleware/v1.pbjson.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/archive/middleware/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports // ignore_for_file: unused_import import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; @$core.Deprecated('Use qnPolicyDescriptor instead') const QnPolicy$json = { '1': 'QnPolicy', '2': [ {'1': 'QN_POLICY_DEFAULT', '2': 0}, {'1': 'QN_POLICY_AUTO_QN_ENABLE', '2': 1}, ], }; /// Descriptor for `QnPolicy`. Decode as a `google.protobuf.EnumDescriptorProto`. final $typed_data.Uint8List qnPolicyDescriptor = $convert.base64Decode( 'CghRblBvbGljeRIVChFRTl9QT0xJQ1lfREVGQVVMVBAAEhwKGFFOX1BPTElDWV9BVVRPX1FOX0' 'VOQUJMRRAB'); @$core.Deprecated('Use playerArgsDescriptor instead') const PlayerArgs$json = { '1': 'PlayerArgs', '2': [ {'1': 'qn', '3': 1, '4': 1, '5': 3, '10': 'qn'}, {'1': 'fnver', '3': 2, '4': 1, '5': 3, '10': 'fnver'}, {'1': 'fnval', '3': 3, '4': 1, '5': 3, '10': 'fnval'}, {'1': 'force_host', '3': 4, '4': 1, '5': 3, '10': 'forceHost'}, {'1': 'voice_balance', '3': 5, '4': 1, '5': 3, '10': 'voiceBalance'}, { '1': 'qn_policy', '3': 6, '4': 1, '5': 14, '6': '.bilibili.app.archive.middleware.v1.QnPolicy', '10': 'qnPolicy' }, ], }; /// Descriptor for `PlayerArgs`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List playerArgsDescriptor = $convert.base64Decode( 'CgpQbGF5ZXJBcmdzEg4KAnFuGAEgASgDUgJxbhIUCgVmbnZlchgCIAEoA1IFZm52ZXISFAoFZm' '52YWwYAyABKANSBWZudmFsEh0KCmZvcmNlX2hvc3QYBCABKANSCWZvcmNlSG9zdBIjCg12b2lj' 'ZV9iYWxhbmNlGAUgASgDUgx2b2ljZUJhbGFuY2USSQoJcW5fcG9saWN5GAYgASgOMiwuYmlsaW' 'JpbGkuYXBwLmFyY2hpdmUubWlkZGxld2FyZS52MS5RblBvbGljeVIIcW5Qb2xpY3k='); ================================================ FILE: lib/grpc/bilibili/app/archive/v1.pb.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/archive/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; class Arc extends $pb.GeneratedMessage { factory Arc({ $fixnum.Int64? aid, $fixnum.Int64? videos, $core.int? typeId, $core.String? typeName, $core.int? copyright, $core.String? pic, $core.String? title, $fixnum.Int64? pubdate, $fixnum.Int64? ctime, $core.String? desc, $core.int? state, $core.int? access, $core.int? attribute, $core.String? tag, $core.Iterable<$core.String>? tags, $fixnum.Int64? duration, $fixnum.Int64? missionId, $fixnum.Int64? orderId, $core.String? redirectUrl, $fixnum.Int64? forward, Rights? rights, Author? author, Stat? stat, $core.String? reportResult, $core.String? dynamic, $fixnum.Int64? firstCid, Dimension? dimension, $core.Iterable? staffInfo, $fixnum.Int64? seasonId, $fixnum.Int64? attributeV2, SeasonTheme? seasonTheme, $core.String? shortLinkV2, $core.int? upFromV2, $core.String? firstFrame, }) { final result = create(); if (aid != null) result.aid = aid; if (videos != null) result.videos = videos; if (typeId != null) result.typeId = typeId; if (typeName != null) result.typeName = typeName; if (copyright != null) result.copyright = copyright; if (pic != null) result.pic = pic; if (title != null) result.title = title; if (pubdate != null) result.pubdate = pubdate; if (ctime != null) result.ctime = ctime; if (desc != null) result.desc = desc; if (state != null) result.state = state; if (access != null) result.access = access; if (attribute != null) result.attribute = attribute; if (tag != null) result.tag = tag; if (tags != null) result.tags.addAll(tags); if (duration != null) result.duration = duration; if (missionId != null) result.missionId = missionId; if (orderId != null) result.orderId = orderId; if (redirectUrl != null) result.redirectUrl = redirectUrl; if (forward != null) result.forward = forward; if (rights != null) result.rights = rights; if (author != null) result.author = author; if (stat != null) result.stat = stat; if (reportResult != null) result.reportResult = reportResult; if (dynamic != null) result.dynamic = dynamic; if (firstCid != null) result.firstCid = firstCid; if (dimension != null) result.dimension = dimension; if (staffInfo != null) result.staffInfo.addAll(staffInfo); if (seasonId != null) result.seasonId = seasonId; if (attributeV2 != null) result.attributeV2 = attributeV2; if (seasonTheme != null) result.seasonTheme = seasonTheme; if (shortLinkV2 != null) result.shortLinkV2 = shortLinkV2; if (upFromV2 != null) result.upFromV2 = upFromV2; if (firstFrame != null) result.firstFrame = firstFrame; return result; } Arc._(); factory Arc.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Arc.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Arc', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'aid') ..aInt64(2, _omitFieldNames ? '' : 'videos') ..aI(3, _omitFieldNames ? '' : 'typeId') ..aOS(4, _omitFieldNames ? '' : 'typeName') ..aI(5, _omitFieldNames ? '' : 'copyright') ..aOS(6, _omitFieldNames ? '' : 'pic') ..aOS(7, _omitFieldNames ? '' : 'title') ..aInt64(8, _omitFieldNames ? '' : 'pubdate') ..aInt64(9, _omitFieldNames ? '' : 'ctime') ..aOS(10, _omitFieldNames ? '' : 'desc') ..aI(11, _omitFieldNames ? '' : 'state') ..aI(12, _omitFieldNames ? '' : 'access') ..aI(13, _omitFieldNames ? '' : 'attribute') ..aOS(14, _omitFieldNames ? '' : 'tag') ..pPS(15, _omitFieldNames ? '' : 'tags') ..aInt64(16, _omitFieldNames ? '' : 'duration') ..aInt64(17, _omitFieldNames ? '' : 'missionId') ..aInt64(18, _omitFieldNames ? '' : 'orderId') ..aOS(19, _omitFieldNames ? '' : 'redirectUrl') ..aInt64(20, _omitFieldNames ? '' : 'forward') ..aOM(21, _omitFieldNames ? '' : 'rights', subBuilder: Rights.create) ..aOM(22, _omitFieldNames ? '' : 'author', subBuilder: Author.create) ..aOM(23, _omitFieldNames ? '' : 'stat', subBuilder: Stat.create) ..aOS(24, _omitFieldNames ? '' : 'reportResult') ..aOS(25, _omitFieldNames ? '' : 'dynamic') ..aInt64(26, _omitFieldNames ? '' : 'firstCid') ..aOM(27, _omitFieldNames ? '' : 'dimension', subBuilder: Dimension.create) ..pPM(28, _omitFieldNames ? '' : 'staffInfo', subBuilder: StaffInfo.create) ..aInt64(29, _omitFieldNames ? '' : 'seasonId') ..aInt64(30, _omitFieldNames ? '' : 'attributeV2') ..aOM(31, _omitFieldNames ? '' : 'seasonTheme', subBuilder: SeasonTheme.create) ..aOS(40, _omitFieldNames ? '' : 'shortLinkV2') ..aI(41, _omitFieldNames ? '' : 'upFromV2') ..aOS(42, _omitFieldNames ? '' : 'firstFrame') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Arc clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Arc copyWith(void Function(Arc) updates) => super.copyWith((message) => updates(message as Arc)) as Arc; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Arc create() => Arc._(); @$core.override Arc createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Arc getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Arc? _defaultInstance; @$pb.TagNumber(1) $fixnum.Int64 get aid => $_getI64(0); @$pb.TagNumber(1) set aid($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasAid() => $_has(0); @$pb.TagNumber(1) void clearAid() => $_clearField(1); /// 分 P 数 @$pb.TagNumber(2) $fixnum.Int64 get videos => $_getI64(1); @$pb.TagNumber(2) set videos($fixnum.Int64 value) => $_setInt64(1, value); @$pb.TagNumber(2) $core.bool hasVideos() => $_has(1); @$pb.TagNumber(2) void clearVideos() => $_clearField(2); /// 分区 ID @$pb.TagNumber(3) $core.int get typeId => $_getIZ(2); @$pb.TagNumber(3) set typeId($core.int value) => $_setSignedInt32(2, value); @$pb.TagNumber(3) $core.bool hasTypeId() => $_has(2); @$pb.TagNumber(3) void clearTypeId() => $_clearField(3); /// 分区名称 @$pb.TagNumber(4) $core.String get typeName => $_getSZ(3); @$pb.TagNumber(4) set typeName($core.String value) => $_setString(3, value); @$pb.TagNumber(4) $core.bool hasTypeName() => $_has(3); @$pb.TagNumber(4) void clearTypeName() => $_clearField(4); /// 是否转载 /// /// - 0: 历史上可能遗留的脏数据 /// - 1: 原创 /// - 2: 转载 @$pb.TagNumber(5) $core.int get copyright => $_getIZ(4); @$pb.TagNumber(5) set copyright($core.int value) => $_setSignedInt32(4, value); @$pb.TagNumber(5) $core.bool hasCopyright() => $_has(4); @$pb.TagNumber(5) void clearCopyright() => $_clearField(5); /// 封面地址 @$pb.TagNumber(6) $core.String get pic => $_getSZ(5); @$pb.TagNumber(6) set pic($core.String value) => $_setString(5, value); @$pb.TagNumber(6) $core.bool hasPic() => $_has(5); @$pb.TagNumber(6) void clearPic() => $_clearField(6); /// 标题 @$pb.TagNumber(7) $core.String get title => $_getSZ(6); @$pb.TagNumber(7) set title($core.String value) => $_setString(6, value); @$pb.TagNumber(7) $core.bool hasTitle() => $_has(6); @$pb.TagNumber(7) void clearTitle() => $_clearField(7); /// 发布时间戳 @$pb.TagNumber(8) $fixnum.Int64 get pubdate => $_getI64(7); @$pb.TagNumber(8) set pubdate($fixnum.Int64 value) => $_setInt64(7, value); @$pb.TagNumber(8) $core.bool hasPubdate() => $_has(7); @$pb.TagNumber(8) void clearPubdate() => $_clearField(8); /// 提交时间戳 @$pb.TagNumber(9) $fixnum.Int64 get ctime => $_getI64(8); @$pb.TagNumber(9) set ctime($fixnum.Int64 value) => $_setInt64(8, value); @$pb.TagNumber(9) $core.bool hasCtime() => $_has(8); @$pb.TagNumber(9) void clearCtime() => $_clearField(9); /// 简介 @$pb.TagNumber(10) $core.String get desc => $_getSZ(9); @$pb.TagNumber(10) set desc($core.String value) => $_setString(9, value); @$pb.TagNumber(10) $core.bool hasDesc() => $_has(9); @$pb.TagNumber(10) void clearDesc() => $_clearField(10); /// 状态 (>= 0 为正常可见状态) @$pb.TagNumber(11) $core.int get state => $_getIZ(10); @$pb.TagNumber(11) set state($core.int value) => $_setSignedInt32(10, value); @$pb.TagNumber(11) $core.bool hasState() => $_has(10); @$pb.TagNumber(11) void clearState() => $_clearField(11); /// 是否可访问 /// /// - 0: 公开 /// - 10000: 仅登录用户 @$pb.TagNumber(12) $core.int get access => $_getIZ(11); @$pb.TagNumber(12) set access($core.int value) => $_setSignedInt32(11, value); @$pb.TagNumber(12) $core.bool hasAccess() => $_has(11); @$pb.TagNumber(12) void clearAccess() => $_clearField(12); /// 属性 @$pb.TagNumber(13) $core.int get attribute => $_getIZ(12); @$pb.TagNumber(13) set attribute($core.int value) => $_setSignedInt32(12, value); @$pb.TagNumber(13) $core.bool hasAttribute() => $_has(12); @$pb.TagNumber(13) void clearAttribute() => $_clearField(13); /// Deprecated @$pb.TagNumber(14) $core.String get tag => $_getSZ(13); @$pb.TagNumber(14) set tag($core.String value) => $_setString(13, value); @$pb.TagNumber(14) $core.bool hasTag() => $_has(13); @$pb.TagNumber(14) void clearTag() => $_clearField(14); /// Deprecated @$pb.TagNumber(15) $pb.PbList<$core.String> get tags => $_getList(14); /// 所有分 P 加起来的总时长 (seconds) @$pb.TagNumber(16) $fixnum.Int64 get duration => $_getI64(15); @$pb.TagNumber(16) set duration($fixnum.Int64 value) => $_setInt64(15, value); @$pb.TagNumber(16) $core.bool hasDuration() => $_has(15); @$pb.TagNumber(16) void clearDuration() => $_clearField(16); /// 参与的活动 id @$pb.TagNumber(17) $fixnum.Int64 get missionId => $_getI64(16); @$pb.TagNumber(17) set missionId($fixnum.Int64 value) => $_setInt64(16, value); @$pb.TagNumber(17) $core.bool hasMissionId() => $_has(16); @$pb.TagNumber(17) void clearMissionId() => $_clearField(17); /// 参与的商单 id @$pb.TagNumber(18) $fixnum.Int64 get orderId => $_getI64(17); @$pb.TagNumber(18) set orderId($fixnum.Int64 value) => $_setInt64(17, value); @$pb.TagNumber(18) $core.bool hasOrderId() => $_has(17); @$pb.TagNumber(18) void clearOrderId() => $_clearField(18); /// 强制跳转地址 @$pb.TagNumber(19) $core.String get redirectUrl => $_getSZ(18); @$pb.TagNumber(19) set redirectUrl($core.String value) => $_setString(18, value); @$pb.TagNumber(19) $core.bool hasRedirectUrl() => $_has(18); @$pb.TagNumber(19) void clearRedirectUrl() => $_clearField(19); @$pb.TagNumber(20) $fixnum.Int64 get forward => $_getI64(19); @$pb.TagNumber(20) set forward($fixnum.Int64 value) => $_setInt64(19, value); @$pb.TagNumber(20) $core.bool hasForward() => $_has(19); @$pb.TagNumber(20) void clearForward() => $_clearField(20); /// 参见 [`Rights`] @$pb.TagNumber(21) Rights get rights => $_getN(20); @$pb.TagNumber(21) set rights(Rights value) => $_setField(21, value); @$pb.TagNumber(21) $core.bool hasRights() => $_has(20); @$pb.TagNumber(21) void clearRights() => $_clearField(21); @$pb.TagNumber(21) Rights ensureRights() => $_ensure(20); /// 稿件作者信息, 参见 [`Author`] @$pb.TagNumber(22) Author get author => $_getN(21); @$pb.TagNumber(22) set author(Author value) => $_setField(22, value); @$pb.TagNumber(22) $core.bool hasAuthor() => $_has(21); @$pb.TagNumber(22) void clearAuthor() => $_clearField(22); @$pb.TagNumber(22) Author ensureAuthor() => $_ensure(21); /// 稿件计数信息, 参见 [`Stat`] @$pb.TagNumber(23) Stat get stat => $_getN(22); @$pb.TagNumber(23) set stat(Stat value) => $_setField(23, value); @$pb.TagNumber(23) $core.bool hasStat() => $_has(22); @$pb.TagNumber(23) void clearStat() => $_clearField(23); @$pb.TagNumber(23) Stat ensureStat() => $_ensure(22); @$pb.TagNumber(24) $core.String get reportResult => $_getSZ(23); @$pb.TagNumber(24) set reportResult($core.String value) => $_setString(23, value); @$pb.TagNumber(24) $core.bool hasReportResult() => $_has(23); @$pb.TagNumber(24) void clearReportResult() => $_clearField(24); /// 发布时动态描述 @$pb.TagNumber(25) $core.String get dynamic => $_getSZ(24); @$pb.TagNumber(25) set dynamic($core.String value) => $_setString(24, value); @$pb.TagNumber(25) $core.bool hasDynamic() => $_has(24); @$pb.TagNumber(25) void clearDynamic() => $_clearField(25); /// 首个分 P 的 cid @$pb.TagNumber(26) $fixnum.Int64 get firstCid => $_getI64(25); @$pb.TagNumber(26) set firstCid($fixnum.Int64 value) => $_setInt64(25, value); @$pb.TagNumber(26) $core.bool hasFirstCid() => $_has(25); @$pb.TagNumber(26) void clearFirstCid() => $_clearField(26); /// 首个分 P 的分辨率 @$pb.TagNumber(27) Dimension get dimension => $_getN(26); @$pb.TagNumber(27) set dimension(Dimension value) => $_setField(27, value); @$pb.TagNumber(27) $core.bool hasDimension() => $_has(26); @$pb.TagNumber(27) void clearDimension() => $_clearField(27); @$pb.TagNumber(27) Dimension ensureDimension() => $_ensure(26); /// 联合投稿信息 @$pb.TagNumber(28) $pb.PbList get staffInfo => $_getList(27); /// UGC 剧集 ID @$pb.TagNumber(29) $fixnum.Int64 get seasonId => $_getI64(28); @$pb.TagNumber(29) set seasonId($fixnum.Int64 value) => $_setInt64(28, value); @$pb.TagNumber(29) $core.bool hasSeasonId() => $_has(28); @$pb.TagNumber(29) void clearSeasonId() => $_clearField(29); /// 属性 (旧的 int32 不够用了) @$pb.TagNumber(30) $fixnum.Int64 get attributeV2 => $_getI64(29); @$pb.TagNumber(30) set attributeV2($fixnum.Int64 value) => $_setInt64(29, value); @$pb.TagNumber(30) $core.bool hasAttributeV2() => $_has(29); @$pb.TagNumber(30) void clearAttributeV2() => $_clearField(30); /// ? UGC 剧集主题 @$pb.TagNumber(31) SeasonTheme get seasonTheme => $_getN(30); @$pb.TagNumber(31) set seasonTheme(SeasonTheme value) => $_setField(31, value); @$pb.TagNumber(31) $core.bool hasSeasonTheme() => $_has(30); @$pb.TagNumber(31) void clearSeasonTheme() => $_clearField(31); @$pb.TagNumber(31) SeasonTheme ensureSeasonTheme() => $_ensure(30); /// ? 短链接 @$pb.TagNumber(40) $core.String get shortLinkV2 => $_getSZ(31); @$pb.TagNumber(40) set shortLinkV2($core.String value) => $_setString(31, value); @$pb.TagNumber(40) $core.bool hasShortLinkV2() => $_has(31); @$pb.TagNumber(40) void clearShortLinkV2() => $_clearField(40); @$pb.TagNumber(41) $core.int get upFromV2 => $_getIZ(32); @$pb.TagNumber(41) set upFromV2($core.int value) => $_setSignedInt32(32, value); @$pb.TagNumber(41) $core.bool hasUpFromV2() => $_has(32); @$pb.TagNumber(41) void clearUpFromV2() => $_clearField(41); @$pb.TagNumber(42) $core.String get firstFrame => $_getSZ(33); @$pb.TagNumber(42) set firstFrame($core.String value) => $_setString(33, value); @$pb.TagNumber(42) $core.bool hasFirstFrame() => $_has(33); @$pb.TagNumber(42) void clearFirstFrame() => $_clearField(42); } /// 作者信息 class Author extends $pb.GeneratedMessage { factory Author({ $fixnum.Int64? mid, $core.String? name, $core.String? face, }) { final result = create(); if (mid != null) result.mid = mid; if (name != null) result.name = name; if (face != null) result.face = face; return result; } Author._(); factory Author.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Author.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Author', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'mid') ..aOS(2, _omitFieldNames ? '' : 'name') ..aOS(3, _omitFieldNames ? '' : 'face') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Author clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Author copyWith(void Function(Author) updates) => super.copyWith((message) => updates(message as Author)) as Author; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Author create() => Author._(); @$core.override Author createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Author getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Author? _defaultInstance; /// UP mid @$pb.TagNumber(1) $fixnum.Int64 get mid => $_getI64(0); @$pb.TagNumber(1) set mid($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasMid() => $_has(0); @$pb.TagNumber(1) void clearMid() => $_clearField(1); /// UP 昵称 @$pb.TagNumber(2) $core.String get name => $_getSZ(1); @$pb.TagNumber(2) set name($core.String value) => $_setString(1, value); @$pb.TagNumber(2) $core.bool hasName() => $_has(1); @$pb.TagNumber(2) void clearName() => $_clearField(2); /// UP 头像 @$pb.TagNumber(3) $core.String get face => $_getSZ(2); @$pb.TagNumber(3) set face($core.String value) => $_setString(2, value); @$pb.TagNumber(3) $core.bool hasFace() => $_has(2); @$pb.TagNumber(3) void clearFace() => $_clearField(3); } /// 视频分辨率 class Dimension extends $pb.GeneratedMessage { factory Dimension({ $fixnum.Int64? width, $fixnum.Int64? height, $fixnum.Int64? rotate, }) { final result = create(); if (width != null) result.width = width; if (height != null) result.height = height; if (rotate != null) result.rotate = rotate; return result; } Dimension._(); factory Dimension.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Dimension.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Dimension', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'width') ..aInt64(2, _omitFieldNames ? '' : 'height') ..aInt64(3, _omitFieldNames ? '' : 'rotate') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Dimension clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Dimension copyWith(void Function(Dimension) updates) => super.copyWith((message) => updates(message as Dimension)) as Dimension; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Dimension create() => Dimension._(); @$core.override Dimension createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Dimension getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Dimension? _defaultInstance; /// 宽 @$pb.TagNumber(1) $fixnum.Int64 get width => $_getI64(0); @$pb.TagNumber(1) set width($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasWidth() => $_has(0); @$pb.TagNumber(1) void clearWidth() => $_clearField(1); /// 高 @$pb.TagNumber(2) $fixnum.Int64 get height => $_getI64(1); @$pb.TagNumber(2) set height($fixnum.Int64 value) => $_setInt64(1, value); @$pb.TagNumber(2) $core.bool hasHeight() => $_has(1); @$pb.TagNumber(2) void clearHeight() => $_clearField(2); /// 是否竖屏 @$pb.TagNumber(3) $fixnum.Int64 get rotate => $_getI64(2); @$pb.TagNumber(3) set rotate($fixnum.Int64 value) => $_setInt64(2, value); @$pb.TagNumber(3) $core.bool hasRotate() => $_has(2); @$pb.TagNumber(3) void clearRotate() => $_clearField(3); } /// 分 P 信息 class Page extends $pb.GeneratedMessage { factory Page({ $fixnum.Int64? cid, $core.int? page, $core.String? from, $core.String? part, $fixnum.Int64? duration, $core.String? vid, $core.String? desc, $core.String? webLink, Dimension? dimension, $core.String? firstFrame, }) { final result = create(); if (cid != null) result.cid = cid; if (page != null) result.page = page; if (from != null) result.from = from; if (part != null) result.part = part; if (duration != null) result.duration = duration; if (vid != null) result.vid = vid; if (desc != null) result.desc = desc; if (webLink != null) result.webLink = webLink; if (dimension != null) result.dimension = dimension; if (firstFrame != null) result.firstFrame = firstFrame; return result; } Page._(); factory Page.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Page.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Page', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'cid') ..aI(2, _omitFieldNames ? '' : 'page') ..aOS(3, _omitFieldNames ? '' : 'from') ..aOS(4, _omitFieldNames ? '' : 'part') ..aInt64(5, _omitFieldNames ? '' : 'duration') ..aOS(6, _omitFieldNames ? '' : 'vid') ..aOS(7, _omitFieldNames ? '' : 'desc') ..aOS(8, _omitFieldNames ? '' : 'webLink') ..aOM(9, _omitFieldNames ? '' : 'dimension', subBuilder: Dimension.create) ..aOS(10, _omitFieldNames ? '' : 'firstFrame') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Page clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Page copyWith(void Function(Page) updates) => super.copyWith((message) => updates(message as Page)) as Page; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Page create() => Page._(); @$core.override Page createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Page getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Page? _defaultInstance; /// 视频流 CID @$pb.TagNumber(1) $fixnum.Int64 get cid => $_getI64(0); @$pb.TagNumber(1) set cid($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasCid() => $_has(0); @$pb.TagNumber(1) void clearCid() => $_clearField(1); /// 视频序号 @$pb.TagNumber(2) $core.int get page => $_getIZ(1); @$pb.TagNumber(2) set page($core.int value) => $_setSignedInt32(1, value); @$pb.TagNumber(2) $core.bool hasPage() => $_has(1); @$pb.TagNumber(2) void clearPage() => $_clearField(2); /// 视频来源 /// /// - vupload /// - qq: Tencent /// - hunan: Hunan TV @$pb.TagNumber(3) $core.String get from => $_getSZ(2); @$pb.TagNumber(3) set from($core.String value) => $_setString(2, value); @$pb.TagNumber(3) $core.bool hasFrom() => $_has(2); @$pb.TagNumber(3) void clearFrom() => $_clearField(3); /// 视频标题 @$pb.TagNumber(4) $core.String get part => $_getSZ(3); @$pb.TagNumber(4) set part($core.String value) => $_setString(3, value); @$pb.TagNumber(4) $core.bool hasPart() => $_has(3); @$pb.TagNumber(4) void clearPart() => $_clearField(4); /// 视频时长 (seconds) @$pb.TagNumber(5) $fixnum.Int64 get duration => $_getI64(4); @$pb.TagNumber(5) set duration($fixnum.Int64 value) => $_setInt64(4, value); @$pb.TagNumber(5) $core.bool hasDuration() => $_has(4); @$pb.TagNumber(5) void clearDuration() => $_clearField(5); /// 站外视频 vid @$pb.TagNumber(6) $core.String get vid => $_getSZ(5); @$pb.TagNumber(6) set vid($core.String value) => $_setString(5, value); @$pb.TagNumber(6) $core.bool hasVid() => $_has(5); @$pb.TagNumber(6) void clearVid() => $_clearField(6); /// 视频简介 @$pb.TagNumber(7) $core.String get desc => $_getSZ(6); @$pb.TagNumber(7) set desc($core.String value) => $_setString(6, value); @$pb.TagNumber(7) $core.bool hasDesc() => $_has(6); @$pb.TagNumber(7) void clearDesc() => $_clearField(7); /// 站外视频跳转地址 @$pb.TagNumber(8) $core.String get webLink => $_getSZ(7); @$pb.TagNumber(8) set webLink($core.String value) => $_setString(7, value); @$pb.TagNumber(8) $core.bool hasWebLink() => $_has(7); @$pb.TagNumber(8) void clearWebLink() => $_clearField(8); /// 见 [`Dimension`] @$pb.TagNumber(9) Dimension get dimension => $_getN(8); @$pb.TagNumber(9) set dimension(Dimension value) => $_setField(9, value); @$pb.TagNumber(9) $core.bool hasDimension() => $_has(8); @$pb.TagNumber(9) void clearDimension() => $_clearField(9); @$pb.TagNumber(9) Dimension ensureDimension() => $_ensure(8); @$pb.TagNumber(10) $core.String get firstFrame => $_getSZ(9); @$pb.TagNumber(10) set firstFrame($core.String value) => $_setString(9, value); @$pb.TagNumber(10) $core.bool hasFirstFrame() => $_has(9); @$pb.TagNumber(10) void clearFirstFrame() => $_clearField(10); } /// 常用属性, 0 否 1 是 class Rights extends $pb.GeneratedMessage { factory Rights({ $core.int? bp, $core.int? elec, $core.int? download, $core.int? movie, $core.int? pay, $core.int? hd5, $core.int? noReprint, $core.int? autoplay, $core.int? ugcPay, $core.int? isCooperation, $core.int? ugcPayPreview, $core.int? noBackground, $core.int? arcPay, $core.int? payFreeWatch, }) { final result = create(); if (bp != null) result.bp = bp; if (elec != null) result.elec = elec; if (download != null) result.download = download; if (movie != null) result.movie = movie; if (pay != null) result.pay = pay; if (hd5 != null) result.hd5 = hd5; if (noReprint != null) result.noReprint = noReprint; if (autoplay != null) result.autoplay = autoplay; if (ugcPay != null) result.ugcPay = ugcPay; if (isCooperation != null) result.isCooperation = isCooperation; if (ugcPayPreview != null) result.ugcPayPreview = ugcPayPreview; if (noBackground != null) result.noBackground = noBackground; if (arcPay != null) result.arcPay = arcPay; if (payFreeWatch != null) result.payFreeWatch = payFreeWatch; return result; } Rights._(); factory Rights.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Rights.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Rights', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aI(1, _omitFieldNames ? '' : 'bp') ..aI(2, _omitFieldNames ? '' : 'elec') ..aI(3, _omitFieldNames ? '' : 'download') ..aI(4, _omitFieldNames ? '' : 'movie') ..aI(5, _omitFieldNames ? '' : 'pay') ..aI(6, _omitFieldNames ? '' : 'hd5') ..aI(7, _omitFieldNames ? '' : 'noReprint') ..aI(8, _omitFieldNames ? '' : 'autoplay') ..aI(9, _omitFieldNames ? '' : 'ugcPay') ..aI(10, _omitFieldNames ? '' : 'isCooperation') ..aI(11, _omitFieldNames ? '' : 'ugcPayPreview') ..aI(12, _omitFieldNames ? '' : 'noBackground') ..aI(13, _omitFieldNames ? '' : 'arcPay') ..aI(14, _omitFieldNames ? '' : 'payFreeWatch') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Rights clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Rights copyWith(void Function(Rights) updates) => super.copyWith((message) => updates(message as Rights)) as Rights; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Rights create() => Rights._(); @$core.override Rights createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Rights getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Rights? _defaultInstance; /// 是否付费(旧版) @$pb.TagNumber(1) $core.int get bp => $_getIZ(0); @$pb.TagNumber(1) set bp($core.int value) => $_setSignedInt32(0, value); @$pb.TagNumber(1) $core.bool hasBp() => $_has(0); @$pb.TagNumber(1) void clearBp() => $_clearField(1); /// 是否支持充电 @$pb.TagNumber(2) $core.int get elec => $_getIZ(1); @$pb.TagNumber(2) set elec($core.int value) => $_setSignedInt32(1, value); @$pb.TagNumber(2) $core.bool hasElec() => $_has(1); @$pb.TagNumber(2) void clearElec() => $_clearField(2); /// 是否下载 @$pb.TagNumber(3) $core.int get download => $_getIZ(2); @$pb.TagNumber(3) set download($core.int value) => $_setSignedInt32(2, value); @$pb.TagNumber(3) $core.bool hasDownload() => $_has(2); @$pb.TagNumber(3) void clearDownload() => $_clearField(3); /// 是否电影 @$pb.TagNumber(4) $core.int get movie => $_getIZ(3); @$pb.TagNumber(4) set movie($core.int value) => $_setSignedInt32(3, value); @$pb.TagNumber(4) $core.bool hasMovie() => $_has(3); @$pb.TagNumber(4) void clearMovie() => $_clearField(4); /// 是否是需要付费的 PGC 稿件 @$pb.TagNumber(5) $core.int get pay => $_getIZ(4); @$pb.TagNumber(5) set pay($core.int value) => $_setSignedInt32(4, value); @$pb.TagNumber(5) $core.bool hasPay() => $_has(4); @$pb.TagNumber(5) void clearPay() => $_clearField(5); /// Deprecated @$pb.TagNumber(6) $core.int get hd5 => $_getIZ(5); @$pb.TagNumber(6) set hd5($core.int value) => $_setSignedInt32(5, value); @$pb.TagNumber(6) $core.bool hasHd5() => $_has(5); @$pb.TagNumber(6) void clearHd5() => $_clearField(6); /// 是否允许转发 @$pb.TagNumber(7) $core.int get noReprint => $_getIZ(6); @$pb.TagNumber(7) set noReprint($core.int value) => $_setSignedInt32(6, value); @$pb.TagNumber(7) $core.bool hasNoReprint() => $_has(6); @$pb.TagNumber(7) void clearNoReprint() => $_clearField(7); /// 是否可以自动播放 @$pb.TagNumber(8) $core.int get autoplay => $_getIZ(7); @$pb.TagNumber(8) set autoplay($core.int value) => $_setSignedInt32(7, value); @$pb.TagNumber(8) $core.bool hasAutoplay() => $_has(7); @$pb.TagNumber(8) void clearAutoplay() => $_clearField(8); /// 是否是需要付费的 UGC 稿件 @$pb.TagNumber(9) $core.int get ugcPay => $_getIZ(8); @$pb.TagNumber(9) set ugcPay($core.int value) => $_setSignedInt32(8, value); @$pb.TagNumber(9) $core.bool hasUgcPay() => $_has(8); @$pb.TagNumber(9) void clearUgcPay() => $_clearField(9); /// 是否联合投稿 @$pb.TagNumber(10) $core.int get isCooperation => $_getIZ(9); @$pb.TagNumber(10) set isCooperation($core.int value) => $_setSignedInt32(9, value); @$pb.TagNumber(10) $core.bool hasIsCooperation() => $_has(9); @$pb.TagNumber(10) void clearIsCooperation() => $_clearField(10); /// 需要付费的 PGC 稿件是否支持预览 @$pb.TagNumber(11) $core.int get ugcPayPreview => $_getIZ(10); @$pb.TagNumber(11) set ugcPayPreview($core.int value) => $_setSignedInt32(10, value); @$pb.TagNumber(11) $core.bool hasUgcPayPreview() => $_has(10); @$pb.TagNumber(11) void clearUgcPayPreview() => $_clearField(11); /// 是否禁止后台播放 @$pb.TagNumber(12) $core.int get noBackground => $_getIZ(11); @$pb.TagNumber(12) set noBackground($core.int value) => $_setSignedInt32(11, value); @$pb.TagNumber(12) $core.bool hasNoBackground() => $_has(11); @$pb.TagNumber(12) void clearNoBackground() => $_clearField(12); @$pb.TagNumber(13) $core.int get arcPay => $_getIZ(12); @$pb.TagNumber(13) set arcPay($core.int value) => $_setSignedInt32(12, value); @$pb.TagNumber(13) $core.bool hasArcPay() => $_has(12); @$pb.TagNumber(13) void clearArcPay() => $_clearField(13); /// ? 需要付费的稿件是否支持免费畅览 @$pb.TagNumber(14) $core.int get payFreeWatch => $_getIZ(13); @$pb.TagNumber(14) set payFreeWatch($core.int value) => $_setSignedInt32(13, value); @$pb.TagNumber(14) $core.bool hasPayFreeWatch() => $_has(13); @$pb.TagNumber(14) void clearPayFreeWatch() => $_clearField(14); } class SeasonTheme extends $pb.GeneratedMessage { factory SeasonTheme({ $core.String? bgColor, $core.String? selectedBgColor, $core.String? textColor, }) { final result = create(); if (bgColor != null) result.bgColor = bgColor; if (selectedBgColor != null) result.selectedBgColor = selectedBgColor; if (textColor != null) result.textColor = textColor; return result; } SeasonTheme._(); factory SeasonTheme.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory SeasonTheme.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'SeasonTheme', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'bgColor') ..aOS(2, _omitFieldNames ? '' : 'selectedBgColor') ..aOS(3, _omitFieldNames ? '' : 'textColor') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') SeasonTheme clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') SeasonTheme copyWith(void Function(SeasonTheme) updates) => super.copyWith((message) => updates(message as SeasonTheme)) as SeasonTheme; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static SeasonTheme create() => SeasonTheme._(); @$core.override SeasonTheme createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static SeasonTheme getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static SeasonTheme? _defaultInstance; @$pb.TagNumber(1) $core.String get bgColor => $_getSZ(0); @$pb.TagNumber(1) set bgColor($core.String value) => $_setString(0, value); @$pb.TagNumber(1) $core.bool hasBgColor() => $_has(0); @$pb.TagNumber(1) void clearBgColor() => $_clearField(1); @$pb.TagNumber(2) $core.String get selectedBgColor => $_getSZ(1); @$pb.TagNumber(2) set selectedBgColor($core.String value) => $_setString(1, value); @$pb.TagNumber(2) $core.bool hasSelectedBgColor() => $_has(1); @$pb.TagNumber(2) void clearSelectedBgColor() => $_clearField(2); @$pb.TagNumber(3) $core.String get textColor => $_getSZ(2); @$pb.TagNumber(3) set textColor($core.String value) => $_setString(2, value); @$pb.TagNumber(3) $core.bool hasTextColor() => $_has(2); @$pb.TagNumber(3) void clearTextColor() => $_clearField(3); } /// 联合投稿成员 class StaffInfo extends $pb.GeneratedMessage { factory StaffInfo({ $fixnum.Int64? mid, $core.String? title, $fixnum.Int64? attribute, }) { final result = create(); if (mid != null) result.mid = mid; if (title != null) result.title = title; if (attribute != null) result.attribute = attribute; return result; } StaffInfo._(); factory StaffInfo.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory StaffInfo.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'StaffInfo', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'mid') ..aOS(2, _omitFieldNames ? '' : 'title') ..aInt64(3, _omitFieldNames ? '' : 'attribute') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') StaffInfo clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') StaffInfo copyWith(void Function(StaffInfo) updates) => super.copyWith((message) => updates(message as StaffInfo)) as StaffInfo; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static StaffInfo create() => StaffInfo._(); @$core.override StaffInfo createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static StaffInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static StaffInfo? _defaultInstance; /// 联合投稿成员 mid @$pb.TagNumber(1) $fixnum.Int64 get mid => $_getI64(0); @$pb.TagNumber(1) set mid($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasMid() => $_has(0); @$pb.TagNumber(1) void clearMid() => $_clearField(1); /// 联合投稿成员角色 @$pb.TagNumber(2) $core.String get title => $_getSZ(1); @$pb.TagNumber(2) set title($core.String value) => $_setString(1, value); @$pb.TagNumber(2) $core.bool hasTitle() => $_has(1); @$pb.TagNumber(2) void clearTitle() => $_clearField(2); /// 属性 @$pb.TagNumber(3) $fixnum.Int64 get attribute => $_getI64(2); @$pb.TagNumber(3) set attribute($fixnum.Int64 value) => $_setInt64(2, value); @$pb.TagNumber(3) $core.bool hasAttribute() => $_has(2); @$pb.TagNumber(3) void clearAttribute() => $_clearField(3); } /// 计数相关信息 class Stat extends $pb.GeneratedMessage { factory Stat({ $fixnum.Int64? aid, $core.int? view, $core.int? danmaku, $core.int? reply, $core.int? fav, $core.int? coin, $core.int? share, $core.int? nowRank, $core.int? hisRank, $core.int? like, $core.int? dislike, }) { final result = create(); if (aid != null) result.aid = aid; if (view != null) result.view = view; if (danmaku != null) result.danmaku = danmaku; if (reply != null) result.reply = reply; if (fav != null) result.fav = fav; if (coin != null) result.coin = coin; if (share != null) result.share = share; if (nowRank != null) result.nowRank = nowRank; if (hisRank != null) result.hisRank = hisRank; if (like != null) result.like = like; if (dislike != null) result.dislike = dislike; return result; } Stat._(); factory Stat.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Stat.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Stat', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.archive.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'aid') ..aI(2, _omitFieldNames ? '' : 'view') ..aI(3, _omitFieldNames ? '' : 'danmaku') ..aI(4, _omitFieldNames ? '' : 'reply') ..aI(5, _omitFieldNames ? '' : 'fav') ..aI(6, _omitFieldNames ? '' : 'coin') ..aI(7, _omitFieldNames ? '' : 'share') ..aI(8, _omitFieldNames ? '' : 'nowRank') ..aI(9, _omitFieldNames ? '' : 'hisRank') ..aI(10, _omitFieldNames ? '' : 'like') ..aI(11, _omitFieldNames ? '' : 'dislike') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Stat clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Stat copyWith(void Function(Stat) updates) => super.copyWith((message) => updates(message as Stat)) as Stat; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Stat create() => Stat._(); @$core.override Stat createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Stat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Stat? _defaultInstance; /// 稿件 avid @$pb.TagNumber(1) $fixnum.Int64 get aid => $_getI64(0); @$pb.TagNumber(1) set aid($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasAid() => $_has(0); @$pb.TagNumber(1) void clearAid() => $_clearField(1); /// 播放量 @$pb.TagNumber(2) $core.int get view => $_getIZ(1); @$pb.TagNumber(2) set view($core.int value) => $_setSignedInt32(1, value); @$pb.TagNumber(2) $core.bool hasView() => $_has(1); @$pb.TagNumber(2) void clearView() => $_clearField(2); /// 弹幕数 @$pb.TagNumber(3) $core.int get danmaku => $_getIZ(2); @$pb.TagNumber(3) set danmaku($core.int value) => $_setSignedInt32(2, value); @$pb.TagNumber(3) $core.bool hasDanmaku() => $_has(2); @$pb.TagNumber(3) void clearDanmaku() => $_clearField(3); /// 评论数 @$pb.TagNumber(4) $core.int get reply => $_getIZ(3); @$pb.TagNumber(4) set reply($core.int value) => $_setSignedInt32(3, value); @$pb.TagNumber(4) $core.bool hasReply() => $_has(3); @$pb.TagNumber(4) void clearReply() => $_clearField(4); /// 收藏数 @$pb.TagNumber(5) $core.int get fav => $_getIZ(4); @$pb.TagNumber(5) set fav($core.int value) => $_setSignedInt32(4, value); @$pb.TagNumber(5) $core.bool hasFav() => $_has(4); @$pb.TagNumber(5) void clearFav() => $_clearField(5); /// 投币数 @$pb.TagNumber(6) $core.int get coin => $_getIZ(5); @$pb.TagNumber(6) set coin($core.int value) => $_setSignedInt32(5, value); @$pb.TagNumber(6) $core.bool hasCoin() => $_has(5); @$pb.TagNumber(6) void clearCoin() => $_clearField(6); /// 分享数 @$pb.TagNumber(7) $core.int get share => $_getIZ(6); @$pb.TagNumber(7) set share($core.int value) => $_setSignedInt32(6, value); @$pb.TagNumber(7) $core.bool hasShare() => $_has(6); @$pb.TagNumber(7) void clearShare() => $_clearField(7); /// 当前排名 @$pb.TagNumber(8) $core.int get nowRank => $_getIZ(7); @$pb.TagNumber(8) set nowRank($core.int value) => $_setSignedInt32(7, value); @$pb.TagNumber(8) $core.bool hasNowRank() => $_has(7); @$pb.TagNumber(8) void clearNowRank() => $_clearField(8); /// 历史最高排名 @$pb.TagNumber(9) $core.int get hisRank => $_getIZ(8); @$pb.TagNumber(9) set hisRank($core.int value) => $_setSignedInt32(8, value); @$pb.TagNumber(9) $core.bool hasHisRank() => $_has(8); @$pb.TagNumber(9) void clearHisRank() => $_clearField(9); /// 点赞数 @$pb.TagNumber(10) $core.int get like => $_getIZ(9); @$pb.TagNumber(10) set like($core.int value) => $_setSignedInt32(9, value); @$pb.TagNumber(10) $core.bool hasLike() => $_has(9); @$pb.TagNumber(10) void clearLike() => $_clearField(10); /// 点踩数 (Deprecated) @$pb.TagNumber(11) $core.int get dislike => $_getIZ(10); @$pb.TagNumber(11) set dislike($core.int value) => $_setSignedInt32(10, value); @$pb.TagNumber(11) $core.bool hasDislike() => $_has(10); @$pb.TagNumber(11) void clearDislike() => $_clearField(11); } const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); ================================================ FILE: lib/grpc/bilibili/app/archive/v1.pbenum.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/archive/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports ================================================ FILE: lib/grpc/bilibili/app/archive/v1.pbjson.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/archive/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports // ignore_for_file: unused_import import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; @$core.Deprecated('Use arcDescriptor instead') const Arc$json = { '1': 'Arc', '2': [ {'1': 'aid', '3': 1, '4': 1, '5': 3, '10': 'aid'}, {'1': 'videos', '3': 2, '4': 1, '5': 3, '10': 'videos'}, {'1': 'type_id', '3': 3, '4': 1, '5': 5, '10': 'typeId'}, {'1': 'type_name', '3': 4, '4': 1, '5': 9, '10': 'typeName'}, {'1': 'copyright', '3': 5, '4': 1, '5': 5, '10': 'copyright'}, {'1': 'pic', '3': 6, '4': 1, '5': 9, '10': 'pic'}, {'1': 'title', '3': 7, '4': 1, '5': 9, '10': 'title'}, {'1': 'pubdate', '3': 8, '4': 1, '5': 3, '10': 'pubdate'}, {'1': 'ctime', '3': 9, '4': 1, '5': 3, '10': 'ctime'}, {'1': 'desc', '3': 10, '4': 1, '5': 9, '10': 'desc'}, {'1': 'state', '3': 11, '4': 1, '5': 5, '10': 'state'}, {'1': 'access', '3': 12, '4': 1, '5': 5, '10': 'access'}, {'1': 'attribute', '3': 13, '4': 1, '5': 5, '10': 'attribute'}, {'1': 'tag', '3': 14, '4': 1, '5': 9, '10': 'tag'}, {'1': 'tags', '3': 15, '4': 3, '5': 9, '10': 'tags'}, {'1': 'duration', '3': 16, '4': 1, '5': 3, '10': 'duration'}, {'1': 'mission_id', '3': 17, '4': 1, '5': 3, '10': 'missionId'}, {'1': 'order_id', '3': 18, '4': 1, '5': 3, '10': 'orderId'}, {'1': 'redirect_url', '3': 19, '4': 1, '5': 9, '10': 'redirectUrl'}, {'1': 'forward', '3': 20, '4': 1, '5': 3, '10': 'forward'}, { '1': 'rights', '3': 21, '4': 1, '5': 11, '6': '.bilibili.app.archive.v1.Rights', '10': 'rights' }, { '1': 'author', '3': 22, '4': 1, '5': 11, '6': '.bilibili.app.archive.v1.Author', '10': 'author' }, { '1': 'stat', '3': 23, '4': 1, '5': 11, '6': '.bilibili.app.archive.v1.Stat', '10': 'stat' }, {'1': 'report_result', '3': 24, '4': 1, '5': 9, '10': 'reportResult'}, {'1': 'dynamic', '3': 25, '4': 1, '5': 9, '10': 'dynamic'}, {'1': 'first_cid', '3': 26, '4': 1, '5': 3, '10': 'firstCid'}, { '1': 'dimension', '3': 27, '4': 1, '5': 11, '6': '.bilibili.app.archive.v1.Dimension', '10': 'dimension' }, { '1': 'staff_info', '3': 28, '4': 3, '5': 11, '6': '.bilibili.app.archive.v1.StaffInfo', '10': 'staffInfo' }, {'1': 'season_id', '3': 29, '4': 1, '5': 3, '10': 'seasonId'}, {'1': 'attribute_v2', '3': 30, '4': 1, '5': 3, '10': 'attributeV2'}, { '1': 'season_theme', '3': 31, '4': 1, '5': 11, '6': '.bilibili.app.archive.v1.SeasonTheme', '10': 'seasonTheme' }, {'1': 'short_link_v2', '3': 40, '4': 1, '5': 9, '10': 'shortLinkV2'}, {'1': 'up_from_v2', '3': 41, '4': 1, '5': 5, '10': 'upFromV2'}, {'1': 'first_frame', '3': 42, '4': 1, '5': 9, '10': 'firstFrame'}, ], }; /// Descriptor for `Arc`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List arcDescriptor = $convert.base64Decode( 'CgNBcmMSEAoDYWlkGAEgASgDUgNhaWQSFgoGdmlkZW9zGAIgASgDUgZ2aWRlb3MSFwoHdHlwZV' '9pZBgDIAEoBVIGdHlwZUlkEhsKCXR5cGVfbmFtZRgEIAEoCVIIdHlwZU5hbWUSHAoJY29weXJp' 'Z2h0GAUgASgFUgljb3B5cmlnaHQSEAoDcGljGAYgASgJUgNwaWMSFAoFdGl0bGUYByABKAlSBX' 'RpdGxlEhgKB3B1YmRhdGUYCCABKANSB3B1YmRhdGUSFAoFY3RpbWUYCSABKANSBWN0aW1lEhIK' 'BGRlc2MYCiABKAlSBGRlc2MSFAoFc3RhdGUYCyABKAVSBXN0YXRlEhYKBmFjY2VzcxgMIAEoBV' 'IGYWNjZXNzEhwKCWF0dHJpYnV0ZRgNIAEoBVIJYXR0cmlidXRlEhAKA3RhZxgOIAEoCVIDdGFn' 'EhIKBHRhZ3MYDyADKAlSBHRhZ3MSGgoIZHVyYXRpb24YECABKANSCGR1cmF0aW9uEh0KCm1pc3' 'Npb25faWQYESABKANSCW1pc3Npb25JZBIZCghvcmRlcl9pZBgSIAEoA1IHb3JkZXJJZBIhCgxy' 'ZWRpcmVjdF91cmwYEyABKAlSC3JlZGlyZWN0VXJsEhgKB2ZvcndhcmQYFCABKANSB2Zvcndhcm' 'QSNwoGcmlnaHRzGBUgASgLMh8uYmlsaWJpbGkuYXBwLmFyY2hpdmUudjEuUmlnaHRzUgZyaWdo' 'dHMSNwoGYXV0aG9yGBYgASgLMh8uYmlsaWJpbGkuYXBwLmFyY2hpdmUudjEuQXV0aG9yUgZhdX' 'Rob3ISMQoEc3RhdBgXIAEoCzIdLmJpbGliaWxpLmFwcC5hcmNoaXZlLnYxLlN0YXRSBHN0YXQS' 'IwoNcmVwb3J0X3Jlc3VsdBgYIAEoCVIMcmVwb3J0UmVzdWx0EhgKB2R5bmFtaWMYGSABKAlSB2' 'R5bmFtaWMSGwoJZmlyc3RfY2lkGBogASgDUghmaXJzdENpZBJACglkaW1lbnNpb24YGyABKAsy' 'Ii5iaWxpYmlsaS5hcHAuYXJjaGl2ZS52MS5EaW1lbnNpb25SCWRpbWVuc2lvbhJBCgpzdGFmZl' '9pbmZvGBwgAygLMiIuYmlsaWJpbGkuYXBwLmFyY2hpdmUudjEuU3RhZmZJbmZvUglzdGFmZklu' 'Zm8SGwoJc2Vhc29uX2lkGB0gASgDUghzZWFzb25JZBIhCgxhdHRyaWJ1dGVfdjIYHiABKANSC2' 'F0dHJpYnV0ZVYyEkcKDHNlYXNvbl90aGVtZRgfIAEoCzIkLmJpbGliaWxpLmFwcC5hcmNoaXZl' 'LnYxLlNlYXNvblRoZW1lUgtzZWFzb25UaGVtZRIiCg1zaG9ydF9saW5rX3YyGCggASgJUgtzaG' '9ydExpbmtWMhIcCgp1cF9mcm9tX3YyGCkgASgFUgh1cEZyb21WMhIfCgtmaXJzdF9mcmFtZRgq' 'IAEoCVIKZmlyc3RGcmFtZQ=='); @$core.Deprecated('Use authorDescriptor instead') const Author$json = { '1': 'Author', '2': [ {'1': 'mid', '3': 1, '4': 1, '5': 3, '10': 'mid'}, {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, {'1': 'face', '3': 3, '4': 1, '5': 9, '10': 'face'}, ], }; /// Descriptor for `Author`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authorDescriptor = $convert.base64Decode( 'CgZBdXRob3ISEAoDbWlkGAEgASgDUgNtaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRmYWNlGA' 'MgASgJUgRmYWNl'); @$core.Deprecated('Use dimensionDescriptor instead') const Dimension$json = { '1': 'Dimension', '2': [ {'1': 'width', '3': 1, '4': 1, '5': 3, '10': 'width'}, {'1': 'height', '3': 2, '4': 1, '5': 3, '10': 'height'}, {'1': 'rotate', '3': 3, '4': 1, '5': 3, '10': 'rotate'}, ], }; /// Descriptor for `Dimension`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List dimensionDescriptor = $convert.base64Decode( 'CglEaW1lbnNpb24SFAoFd2lkdGgYASABKANSBXdpZHRoEhYKBmhlaWdodBgCIAEoA1IGaGVpZ2' 'h0EhYKBnJvdGF0ZRgDIAEoA1IGcm90YXRl'); @$core.Deprecated('Use pageDescriptor instead') const Page$json = { '1': 'Page', '2': [ {'1': 'cid', '3': 1, '4': 1, '5': 3, '10': 'cid'}, {'1': 'page', '3': 2, '4': 1, '5': 5, '10': 'page'}, {'1': 'from', '3': 3, '4': 1, '5': 9, '10': 'from'}, {'1': 'part', '3': 4, '4': 1, '5': 9, '10': 'part'}, {'1': 'duration', '3': 5, '4': 1, '5': 3, '10': 'duration'}, {'1': 'vid', '3': 6, '4': 1, '5': 9, '10': 'vid'}, {'1': 'desc', '3': 7, '4': 1, '5': 9, '10': 'desc'}, {'1': 'web_link', '3': 8, '4': 1, '5': 9, '10': 'webLink'}, { '1': 'dimension', '3': 9, '4': 1, '5': 11, '6': '.bilibili.app.archive.v1.Dimension', '10': 'dimension' }, {'1': 'first_frame', '3': 10, '4': 1, '5': 9, '10': 'firstFrame'}, ], }; /// Descriptor for `Page`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List pageDescriptor = $convert.base64Decode( 'CgRQYWdlEhAKA2NpZBgBIAEoA1IDY2lkEhIKBHBhZ2UYAiABKAVSBHBhZ2USEgoEZnJvbRgDIA' 'EoCVIEZnJvbRISCgRwYXJ0GAQgASgJUgRwYXJ0EhoKCGR1cmF0aW9uGAUgASgDUghkdXJhdGlv' 'bhIQCgN2aWQYBiABKAlSA3ZpZBISCgRkZXNjGAcgASgJUgRkZXNjEhkKCHdlYl9saW5rGAggAS' 'gJUgd3ZWJMaW5rEkAKCWRpbWVuc2lvbhgJIAEoCzIiLmJpbGliaWxpLmFwcC5hcmNoaXZlLnYx' 'LkRpbWVuc2lvblIJZGltZW5zaW9uEh8KC2ZpcnN0X2ZyYW1lGAogASgJUgpmaXJzdEZyYW1l'); @$core.Deprecated('Use rightsDescriptor instead') const Rights$json = { '1': 'Rights', '2': [ {'1': 'bp', '3': 1, '4': 1, '5': 5, '10': 'bp'}, {'1': 'elec', '3': 2, '4': 1, '5': 5, '10': 'elec'}, {'1': 'download', '3': 3, '4': 1, '5': 5, '10': 'download'}, {'1': 'movie', '3': 4, '4': 1, '5': 5, '10': 'movie'}, {'1': 'pay', '3': 5, '4': 1, '5': 5, '10': 'pay'}, {'1': 'hd5', '3': 6, '4': 1, '5': 5, '10': 'hd5'}, {'1': 'no_reprint', '3': 7, '4': 1, '5': 5, '10': 'noReprint'}, {'1': 'autoplay', '3': 8, '4': 1, '5': 5, '10': 'autoplay'}, {'1': 'ugc_pay', '3': 9, '4': 1, '5': 5, '10': 'ugcPay'}, {'1': 'is_cooperation', '3': 10, '4': 1, '5': 5, '10': 'isCooperation'}, {'1': 'ugc_pay_preview', '3': 11, '4': 1, '5': 5, '10': 'ugcPayPreview'}, {'1': 'no_background', '3': 12, '4': 1, '5': 5, '10': 'noBackground'}, {'1': 'arc_pay', '3': 13, '4': 1, '5': 5, '10': 'arcPay'}, {'1': 'pay_free_watch', '3': 14, '4': 1, '5': 5, '10': 'payFreeWatch'}, ], }; /// Descriptor for `Rights`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List rightsDescriptor = $convert.base64Decode( 'CgZSaWdodHMSDgoCYnAYASABKAVSAmJwEhIKBGVsZWMYAiABKAVSBGVsZWMSGgoIZG93bmxvYW' 'QYAyABKAVSCGRvd25sb2FkEhQKBW1vdmllGAQgASgFUgVtb3ZpZRIQCgNwYXkYBSABKAVSA3Bh' 'eRIQCgNoZDUYBiABKAVSA2hkNRIdCgpub19yZXByaW50GAcgASgFUglub1JlcHJpbnQSGgoIYX' 'V0b3BsYXkYCCABKAVSCGF1dG9wbGF5EhcKB3VnY19wYXkYCSABKAVSBnVnY1BheRIlCg5pc19j' 'b29wZXJhdGlvbhgKIAEoBVINaXNDb29wZXJhdGlvbhImCg91Z2NfcGF5X3ByZXZpZXcYCyABKA' 'VSDXVnY1BheVByZXZpZXcSIwoNbm9fYmFja2dyb3VuZBgMIAEoBVIMbm9CYWNrZ3JvdW5kEhcK' 'B2FyY19wYXkYDSABKAVSBmFyY1BheRIkCg5wYXlfZnJlZV93YXRjaBgOIAEoBVIMcGF5RnJlZV' 'dhdGNo'); @$core.Deprecated('Use seasonThemeDescriptor instead') const SeasonTheme$json = { '1': 'SeasonTheme', '2': [ {'1': 'bg_color', '3': 1, '4': 1, '5': 9, '10': 'bgColor'}, {'1': 'selected_bg_color', '3': 2, '4': 1, '5': 9, '10': 'selectedBgColor'}, {'1': 'text_color', '3': 3, '4': 1, '5': 9, '10': 'textColor'}, ], }; /// Descriptor for `SeasonTheme`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List seasonThemeDescriptor = $convert.base64Decode( 'CgtTZWFzb25UaGVtZRIZCghiZ19jb2xvchgBIAEoCVIHYmdDb2xvchIqChFzZWxlY3RlZF9iZ1' '9jb2xvchgCIAEoCVIPc2VsZWN0ZWRCZ0NvbG9yEh0KCnRleHRfY29sb3IYAyABKAlSCXRleHRD' 'b2xvcg=='); @$core.Deprecated('Use staffInfoDescriptor instead') const StaffInfo$json = { '1': 'StaffInfo', '2': [ {'1': 'mid', '3': 1, '4': 1, '5': 3, '10': 'mid'}, {'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'}, {'1': 'attribute', '3': 3, '4': 1, '5': 3, '10': 'attribute'}, ], }; /// Descriptor for `StaffInfo`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List staffInfoDescriptor = $convert.base64Decode( 'CglTdGFmZkluZm8SEAoDbWlkGAEgASgDUgNtaWQSFAoFdGl0bGUYAiABKAlSBXRpdGxlEhwKCW' 'F0dHJpYnV0ZRgDIAEoA1IJYXR0cmlidXRl'); @$core.Deprecated('Use statDescriptor instead') const Stat$json = { '1': 'Stat', '2': [ {'1': 'aid', '3': 1, '4': 1, '5': 3, '10': 'aid'}, {'1': 'view', '3': 2, '4': 1, '5': 5, '10': 'view'}, {'1': 'danmaku', '3': 3, '4': 1, '5': 5, '10': 'danmaku'}, {'1': 'reply', '3': 4, '4': 1, '5': 5, '10': 'reply'}, {'1': 'fav', '3': 5, '4': 1, '5': 5, '10': 'fav'}, {'1': 'coin', '3': 6, '4': 1, '5': 5, '10': 'coin'}, {'1': 'share', '3': 7, '4': 1, '5': 5, '10': 'share'}, {'1': 'now_rank', '3': 8, '4': 1, '5': 5, '10': 'nowRank'}, {'1': 'his_rank', '3': 9, '4': 1, '5': 5, '10': 'hisRank'}, {'1': 'like', '3': 10, '4': 1, '5': 5, '10': 'like'}, {'1': 'dislike', '3': 11, '4': 1, '5': 5, '10': 'dislike'}, ], }; /// Descriptor for `Stat`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List statDescriptor = $convert.base64Decode( 'CgRTdGF0EhAKA2FpZBgBIAEoA1IDYWlkEhIKBHZpZXcYAiABKAVSBHZpZXcSGAoHZGFubWFrdR' 'gDIAEoBVIHZGFubWFrdRIUCgVyZXBseRgEIAEoBVIFcmVwbHkSEAoDZmF2GAUgASgFUgNmYXYS' 'EgoEY29pbhgGIAEoBVIEY29pbhIUCgVzaGFyZRgHIAEoBVIFc2hhcmUSGQoIbm93X3JhbmsYCC' 'ABKAVSB25vd1JhbmsSGQoIaGlzX3JhbmsYCSABKAVSB2hpc1JhbmsSEgoEbGlrZRgKIAEoBVIE' 'bGlrZRIYCgdkaXNsaWtlGAsgASgFUgdkaXNsaWtl'); ================================================ FILE: lib/grpc/bilibili/app/card/v1.pb.dart ================================================ // This is a generated file - do not edit. // // Generated from bilibili/app/card/v1.proto. // @dart = 3.3 // ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; class AdInfo extends $pb.GeneratedMessage { factory AdInfo({ $fixnum.Int64? creativeId, $core.int? creativeType, $core.int? cardType, CreativeContent? creativeContent, $core.String? adCb, $fixnum.Int64? resource, $core.int? source, $core.String? requestId, $core.bool? isAd, $fixnum.Int64? cmMark, $core.int? index, $core.bool? isAdLoc, $core.int? cardIndex, $core.String? clientIp, $core.List<$core.int>? extra, $core.int? creativeStyle, $fixnum.Int64? natureAd, $core.int? contentFastAccess, }) { final result = create(); if (creativeId != null) result.creativeId = creativeId; if (creativeType != null) result.creativeType = creativeType; if (cardType != null) result.cardType = cardType; if (creativeContent != null) result.creativeContent = creativeContent; if (adCb != null) result.adCb = adCb; if (resource != null) result.resource = resource; if (source != null) result.source = source; if (requestId != null) result.requestId = requestId; if (isAd != null) result.isAd = isAd; if (cmMark != null) result.cmMark = cmMark; if (index != null) result.index = index; if (isAdLoc != null) result.isAdLoc = isAdLoc; if (cardIndex != null) result.cardIndex = cardIndex; if (clientIp != null) result.clientIp = clientIp; if (extra != null) result.extra = extra; if (creativeStyle != null) result.creativeStyle = creativeStyle; if (natureAd != null) result.natureAd = natureAd; if (contentFastAccess != null) result.contentFastAccess = contentFastAccess; return result; } AdInfo._(); factory AdInfo.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory AdInfo.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'AdInfo', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.card.v1'), createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'creativeId') ..aI(2, _omitFieldNames ? '' : 'creativeType') ..aI(3, _omitFieldNames ? '' : 'cardType') ..aOM(4, _omitFieldNames ? '' : 'creativeContent', subBuilder: CreativeContent.create) ..aOS(5, _omitFieldNames ? '' : 'adCb') ..aInt64(6, _omitFieldNames ? '' : 'resource') ..aI(7, _omitFieldNames ? '' : 'source') ..aOS(8, _omitFieldNames ? '' : 'requestId') ..aOB(9, _omitFieldNames ? '' : 'isAd') ..aInt64(10, _omitFieldNames ? '' : 'cmMark') ..aI(11, _omitFieldNames ? '' : 'index') ..aOB(12, _omitFieldNames ? '' : 'isAdLoc') ..aI(13, _omitFieldNames ? '' : 'cardIndex') ..aOS(14, _omitFieldNames ? '' : 'clientIp') ..a<$core.List<$core.int>>( 15, _omitFieldNames ? '' : 'extra', $pb.PbFieldType.OY) ..aI(16, _omitFieldNames ? '' : 'creativeStyle') ..aInt64(17, _omitFieldNames ? '' : 'natureAd') ..aI(18, _omitFieldNames ? '' : 'contentFastAccess') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') AdInfo clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') AdInfo copyWith(void Function(AdInfo) updates) => super.copyWith((message) => updates(message as AdInfo)) as AdInfo; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static AdInfo create() => AdInfo._(); @$core.override AdInfo createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static AdInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static AdInfo? _defaultInstance; @$pb.TagNumber(1) $fixnum.Int64 get creativeId => $_getI64(0); @$pb.TagNumber(1) set creativeId($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) $core.bool hasCreativeId() => $_has(0); @$pb.TagNumber(1) void clearCreativeId() => $_clearField(1); @$pb.TagNumber(2) $core.int get creativeType => $_getIZ(1); @$pb.TagNumber(2) set creativeType($core.int value) => $_setSignedInt32(1, value); @$pb.TagNumber(2) $core.bool hasCreativeType() => $_has(1); @$pb.TagNumber(2) void clearCreativeType() => $_clearField(2); @$pb.TagNumber(3) $core.int get cardType => $_getIZ(2); @$pb.TagNumber(3) set cardType($core.int value) => $_setSignedInt32(2, value); @$pb.TagNumber(3) $core.bool hasCardType() => $_has(2); @$pb.TagNumber(3) void clearCardType() => $_clearField(3); @$pb.TagNumber(4) CreativeContent get creativeContent => $_getN(3); @$pb.TagNumber(4) set creativeContent(CreativeContent value) => $_setField(4, value); @$pb.TagNumber(4) $core.bool hasCreativeContent() => $_has(3); @$pb.TagNumber(4) void clearCreativeContent() => $_clearField(4); @$pb.TagNumber(4) CreativeContent ensureCreativeContent() => $_ensure(3); @$pb.TagNumber(5) $core.String get adCb => $_getSZ(4); @$pb.TagNumber(5) set adCb($core.String value) => $_setString(4, value); @$pb.TagNumber(5) $core.bool hasAdCb() => $_has(4); @$pb.TagNumber(5) void clearAdCb() => $_clearField(5); @$pb.TagNumber(6) $fixnum.Int64 get resource => $_getI64(5); @$pb.TagNumber(6) set resource($fixnum.Int64 value) => $_setInt64(5, value); @$pb.TagNumber(6) $core.bool hasResource() => $_has(5); @$pb.TagNumber(6) void clearResource() => $_clearField(6); @$pb.TagNumber(7) $core.int get source => $_getIZ(6); @$pb.TagNumber(7) set source($core.int value) => $_setSignedInt32(6, value); @$pb.TagNumber(7) $core.bool hasSource() => $_has(6); @$pb.TagNumber(7) void clearSource() => $_clearField(7); @$pb.TagNumber(8) $core.String get requestId => $_getSZ(7); @$pb.TagNumber(8) set requestId($core.String value) => $_setString(7, value); @$pb.TagNumber(8) $core.bool hasRequestId() => $_has(7); @$pb.TagNumber(8) void clearRequestId() => $_clearField(8); @$pb.TagNumber(9) $core.bool get isAd => $_getBF(8); @$pb.TagNumber(9) set isAd($core.bool value) => $_setBool(8, value); @$pb.TagNumber(9) $core.bool hasIsAd() => $_has(8); @$pb.TagNumber(9) void clearIsAd() => $_clearField(9); @$pb.TagNumber(10) $fixnum.Int64 get cmMark => $_getI64(9); @$pb.TagNumber(10) set cmMark($fixnum.Int64 value) => $_setInt64(9, value); @$pb.TagNumber(10) $core.bool hasCmMark() => $_has(9); @$pb.TagNumber(10) void clearCmMark() => $_clearField(10); @$pb.TagNumber(11) $core.int get index => $_getIZ(10); @$pb.TagNumber(11) set index($core.int value) => $_setSignedInt32(10, value); @$pb.TagNumber(11) $core.bool hasIndex() => $_has(10); @$pb.TagNumber(11) void clearIndex() => $_clearField(11); @$pb.TagNumber(12) $core.bool get isAdLoc => $_getBF(11); @$pb.TagNumber(12) set isAdLoc($core.bool value) => $_setBool(11, value); @$pb.TagNumber(12) $core.bool hasIsAdLoc() => $_has(11); @$pb.TagNumber(12) void clearIsAdLoc() => $_clearField(12); @$pb.TagNumber(13) $core.int get cardIndex => $_getIZ(12); @$pb.TagNumber(13) set cardIndex($core.int value) => $_setSignedInt32(12, value); @$pb.TagNumber(13) $core.bool hasCardIndex() => $_has(12); @$pb.TagNumber(13) void clearCardIndex() => $_clearField(13); @$pb.TagNumber(14) $core.String get clientIp => $_getSZ(13); @$pb.TagNumber(14) set clientIp($core.String value) => $_setString(13, value); @$pb.TagNumber(14) $core.bool hasClientIp() => $_has(13); @$pb.TagNumber(14) void clearClientIp() => $_clearField(14); @$pb.TagNumber(15) $core.List<$core.int> get extra => $_getN(14); @$pb.TagNumber(15) set extra($core.List<$core.int> value) => $_setBytes(14, value); @$pb.TagNumber(15) $core.bool hasExtra() => $_has(14); @$pb.TagNumber(15) void clearExtra() => $_clearField(15); @$pb.TagNumber(16) $core.int get creativeStyle => $_getIZ(15); @$pb.TagNumber(16) set creativeStyle($core.int value) => $_setSignedInt32(15, value); @$pb.TagNumber(16) $core.bool hasCreativeStyle() => $_has(15); @$pb.TagNumber(16) void clearCreativeStyle() => $_clearField(16); @$pb.TagNumber(17) $fixnum.Int64 get natureAd => $_getI64(16); @$pb.TagNumber(17) set natureAd($fixnum.Int64 value) => $_setInt64(16, value); @$pb.TagNumber(17) $core.bool hasNatureAd() => $_has(16); @$pb.TagNumber(17) void clearNatureAd() => $_clearField(17); @$pb.TagNumber(18) $core.int get contentFastAccess => $_getIZ(17); @$pb.TagNumber(18) set contentFastAccess($core.int value) => $_setSignedInt32(17, value); @$pb.TagNumber(18) $core.bool hasContentFastAccess() => $_has(17); @$pb.TagNumber(18) void clearContentFastAccess() => $_clearField(18); } class Args extends $pb.GeneratedMessage { factory Args({ $core.int? type, $fixnum.Int64? upId, $core.String? upName, $core.int? rid, $core.String? rname, $fixnum.Int64? tid, $core.String? tname, $core.String? trackId, $core.String? state, $core.int? convergeType, $fixnum.Int64? aid, }) { final result = create(); if (type != null) result.type = type; if (upId != null) result.upId = upId; if (upName != null) result.upName = upName; if (rid != null) result.rid = rid; if (rname != null) result.rname = rname; if (tid != null) result.tid = tid; if (tname != null) result.tname = tname; if (trackId != null) result.trackId = trackId; if (state != null) result.state = state; if (convergeType != null) result.convergeType = convergeType; if (aid != null) result.aid = aid; return result; } Args._(); factory Args.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Args.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Args', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.card.v1'), createEmptyInstance: create) ..aI(1, _omitFieldNames ? '' : 'type') ..aInt64(2, _omitFieldNames ? '' : 'upId') ..aOS(3, _omitFieldNames ? '' : 'upName') ..aI(4, _omitFieldNames ? '' : 'rid') ..aOS(5, _omitFieldNames ? '' : 'rname') ..aInt64(6, _omitFieldNames ? '' : 'tid') ..aOS(7, _omitFieldNames ? '' : 'tname') ..aOS(8, _omitFieldNames ? '' : 'trackId') ..aOS(9, _omitFieldNames ? '' : 'state') ..aI(10, _omitFieldNames ? '' : 'convergeType') ..aInt64(11, _omitFieldNames ? '' : 'aid') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Args clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Args copyWith(void Function(Args) updates) => super.copyWith((message) => updates(message as Args)) as Args; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Args create() => Args._(); @$core.override Args createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Args getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Args? _defaultInstance; @$pb.TagNumber(1) $core.int get type => $_getIZ(0); @$pb.TagNumber(1) set type($core.int value) => $_setSignedInt32(0, value); @$pb.TagNumber(1) $core.bool hasType() => $_has(0); @$pb.TagNumber(1) void clearType() => $_clearField(1); @$pb.TagNumber(2) $fixnum.Int64 get upId => $_getI64(1); @$pb.TagNumber(2) set upId($fixnum.Int64 value) => $_setInt64(1, value); @$pb.TagNumber(2) $core.bool hasUpId() => $_has(1); @$pb.TagNumber(2) void clearUpId() => $_clearField(2); @$pb.TagNumber(3) $core.String get upName => $_getSZ(2); @$pb.TagNumber(3) set upName($core.String value) => $_setString(2, value); @$pb.TagNumber(3) $core.bool hasUpName() => $_has(2); @$pb.TagNumber(3) void clearUpName() => $_clearField(3); @$pb.TagNumber(4) $core.int get rid => $_getIZ(3); @$pb.TagNumber(4) set rid($core.int value) => $_setSignedInt32(3, value); @$pb.TagNumber(4) $core.bool hasRid() => $_has(3); @$pb.TagNumber(4) void clearRid() => $_clearField(4); @$pb.TagNumber(5) $core.String get rname => $_getSZ(4); @$pb.TagNumber(5) set rname($core.String value) => $_setString(4, value); @$pb.TagNumber(5) $core.bool hasRname() => $_has(4); @$pb.TagNumber(5) void clearRname() => $_clearField(5); @$pb.TagNumber(6) $fixnum.Int64 get tid => $_getI64(5); @$pb.TagNumber(6) set tid($fixnum.Int64 value) => $_setInt64(5, value); @$pb.TagNumber(6) $core.bool hasTid() => $_has(5); @$pb.TagNumber(6) void clearTid() => $_clearField(6); @$pb.TagNumber(7) $core.String get tname => $_getSZ(6); @$pb.TagNumber(7) set tname($core.String value) => $_setString(6, value); @$pb.TagNumber(7) $core.bool hasTname() => $_has(6); @$pb.TagNumber(7) void clearTname() => $_clearField(7); @$pb.TagNumber(8) $core.String get trackId => $_getSZ(7); @$pb.TagNumber(8) set trackId($core.String value) => $_setString(7, value); @$pb.TagNumber(8) $core.bool hasTrackId() => $_has(7); @$pb.TagNumber(8) void clearTrackId() => $_clearField(8); @$pb.TagNumber(9) $core.String get state => $_getSZ(8); @$pb.TagNumber(9) set state($core.String value) => $_setString(8, value); @$pb.TagNumber(9) $core.bool hasState() => $_has(8); @$pb.TagNumber(9) void clearState() => $_clearField(9); @$pb.TagNumber(10) $core.int get convergeType => $_getIZ(9); @$pb.TagNumber(10) set convergeType($core.int value) => $_setSignedInt32(9, value); @$pb.TagNumber(10) $core.bool hasConvergeType() => $_has(9); @$pb.TagNumber(10) void clearConvergeType() => $_clearField(10); @$pb.TagNumber(11) $fixnum.Int64 get aid => $_getI64(10); @$pb.TagNumber(11) set aid($fixnum.Int64 value) => $_setInt64(10, value); @$pb.TagNumber(11) $core.bool hasAid() => $_has(10); @$pb.TagNumber(11) void clearAid() => $_clearField(11); } class Avatar extends $pb.GeneratedMessage { factory Avatar({ $core.String? cover, $core.String? text, $core.String? uri, $core.int? type, $core.String? event, $core.String? eventV2, $core.int? defalutCover, }) { final result = create(); if (cover != null) result.cover = cover; if (text != null) result.text = text; if (uri != null) result.uri = uri; if (type != null) result.type = type; if (event != null) result.event = event; if (eventV2 != null) result.eventV2 = eventV2; if (defalutCover != null) result.defalutCover = defalutCover; return result; } Avatar._(); factory Avatar.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Avatar.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Avatar', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.card.v1'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'cover') ..aOS(2, _omitFieldNames ? '' : 'text') ..aOS(3, _omitFieldNames ? '' : 'uri') ..aI(4, _omitFieldNames ? '' : 'type') ..aOS(5, _omitFieldNames ? '' : 'event') ..aOS(6, _omitFieldNames ? '' : 'eventV2') ..aI(7, _omitFieldNames ? '' : 'defalutCover') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Avatar clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') Avatar copyWith(void Function(Avatar) updates) => super.copyWith((message) => updates(message as Avatar)) as Avatar; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Avatar create() => Avatar._(); @$core.override Avatar createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') static Avatar getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Avatar? _defaultInstance; @$pb.TagNumber(1) $core.String get cover => $_getSZ(0); @$pb.TagNumber(1) set cover($core.String value) => $_setString(0, value); @$pb.TagNumber(1) $core.bool hasCover() => $_has(0); @$pb.TagNumber(1) void clearCover() => $_clearField(1); @$pb.TagNumber(2) $core.String get text => $_getSZ(1); @$pb.TagNumber(2) set text($core.String value) => $_setString(1, value); @$pb.TagNumber(2) $core.bool hasText() => $_has(1); @$pb.TagNumber(2) void clearText() => $_clearField(2); @$pb.TagNumber(3) $core.String get uri => $_getSZ(2); @$pb.TagNumber(3) set uri($core.String value) => $_setString(2, value); @$pb.TagNumber(3) $core.bool hasUri() => $_has(2); @$pb.TagNumber(3) void clearUri() => $_clearField(3); @$pb.TagNumber(4) $core.int get type => $_getIZ(3); @$pb.TagNumber(4) set type($core.int value) => $_setSignedInt32(3, value); @$pb.TagNumber(4) $core.bool hasType() => $_has(3); @$pb.TagNumber(4) void clearType() => $_clearField(4); @$pb.TagNumber(5) $core.String get event => $_getSZ(4); @$pb.TagNumber(5) set event($core.String value) => $_setString(4, value); @$pb.TagNumber(5) $core.bool hasEvent() => $_has(4); @$pb.TagNumber(5) void clearEvent() => $_clearField(5); @$pb.TagNumber(6) $core.String get eventV2 => $_getSZ(5); @$pb.TagNumber(6) set eventV2($core.String value) => $_setString(5, value); @$pb.TagNumber(6) $core.bool hasEventV2() => $_has(5); @$pb.TagNumber(6) void clearEventV2() => $_clearField(6); @$pb.TagNumber(7) $core.int get defalutCover => $_getIZ(6); @$pb.TagNumber(7) set defalutCover($core.int value) => $_setSignedInt32(6, value); @$pb.TagNumber(7) $core.bool hasDefalutCover() => $_has(6); @$pb.TagNumber(7) void clearDefalutCover() => $_clearField(7); } class Base extends $pb.GeneratedMessage { factory Base({ $core.String? cardType, $core.String? cardGoto, $core.String? goto, $core.String? param, $core.String? cover, $core.String? title, $core.String? uri, ThreePoint? threePoint, Args? args, PlayerArgs? playerArgs, $fixnum.Int64? idx, AdInfo? adInfo, Mask? mask, $core.String? fromType, $core.Iterable? threePointV2, $core.Iterable? threePointV3, Button? descButton, ThreePointV4? threePointV4, UpArgs? upArgs, $core.String? trackId, }) { final result = create(); if (cardType != null) result.cardType = cardType; if (cardGoto != null) result.cardGoto = cardGoto; if (goto != null) result.goto = goto; if (param != null) result.param = param; if (cover != null) result.cover = cover; if (title != null) result.title = title; if (uri != null) result.uri = uri; if (threePoint != null) result.threePoint = threePoint; if (args != null) result.args = args; if (playerArgs != null) result.playerArgs = playerArgs; if (idx != null) result.idx = idx; if (adInfo != null) result.adInfo = adInfo; if (mask != null) result.mask = mask; if (fromType != null) result.fromType = fromType; if (threePointV2 != null) result.threePointV2.addAll(threePointV2); if (threePointV3 != null) result.threePointV3.addAll(threePointV3); if (descButton != null) result.descButton = descButton; if (threePointV4 != null) result.threePointV4 = threePointV4; if (upArgs != null) result.upArgs = upArgs; if (trackId != null) result.trackId = trackId; return result; } Base._(); factory Base.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); factory Base.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'Base', package: const $pb.PackageName( _omitMessageNames ? '' : 'bilibili.app.card.v1'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'cardType') ..aOS(2, _omitFieldNames ? '' : 'cardGoto') ..aOS(3, _omitFieldNames ? '' : 'goto') ..aOS(4, _omitFieldNames ? '' : 'param') ..aOS(5, _omitFieldNames ? '' : 'cover') ..aOS(6, _omitFieldNames ? '' : 'title') ..aOS(7, _omitFieldNames ? '' : 'uri') ..aOM(8, _omitFieldNames ? '' : 'threePoint', subBuilder: ThreePoint.create) ..aOM(9, _omitFieldNames ? '' : 'args', subBuilder: Args.create) ..aOM(10, _omitFieldNames ? '' : 'playerArgs', subBuilder: PlayerArgs.create) ..aInt64(11, _omitFieldNames ? '' : 'idx') ..aOM(12, _omitFieldNames ? '' : 'adInfo', subBuilder: AdInfo.create) ..aOM(13, _omitFieldNames ? '' : 'mask', subBuilder: Mask.create) ..aOS(14, _omitFieldNames ? '' : 'fromType') ..pPM(15, _omitFieldNames ? '' : 'threePointV2', subBuilder: ThreePointV2.create) ..pPM(16, _omitFieldNames ? '' : 'threePointV3', subBuilder: ThreePointV3.create) ..aOM