From c337f72c49fa1593b0457b980986c9c374f72256 Mon Sep 17 00:00:00 2001 From: "xj.Deng" <408232927@qq.com> Date: Thu, 31 Oct 2019 11:59:18 +0800 Subject: [PATCH 1/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f309034..ebf28524 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Language: [English](https://github.com/alibaba/flutter-go/blob/master/README-en. - [命令行 生成 `Flutter Go Widget` 标准公共模版](https://github.com/alibaba/flutter-go/blob/beta/docs/widget.md) - `markdown` 模版动态化生成(合并到master分支后) - [x] ` Flutter Go ` 官方 `APP` 版本自动升级 -- [ ] 多端模版同步( pc端,native端同步 ) +- [x] 多端模版同步( pc端,native端同步 ) - [ ] `Flutter Go` store From 234a438b707a67eb8d0d87d1d8cca26034b55afa Mon Sep 17 00:00:00 2001 From: zymxxxs Date: Fri, 8 Nov 2019 00:32:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20iOS=20=E7=9B=B8=E5=85=B3=E6=94=B9?= =?UTF-8?q?=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. webview 增加 loading 的功能,同时解决 present 试图,动画不正确的问题 2. 修复 ‘关于手册页面’,GitHub 按钮遮挡状态栏的问题 Signed-off-by: cangzhu --- lib/views/fourth_page/pages.dart | 3 ++- lib/views/web_page/web_view_page.dart | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/views/fourth_page/pages.dart b/lib/views/fourth_page/pages.dart index 5d088e07..b74a6cef 100644 --- a/lib/views/fourth_page/pages.dart +++ b/lib/views/fourth_page/pages.dart @@ -71,6 +71,7 @@ class Page extends StatelessWidget { @override Widget build(BuildContext context) { + final paddingTop = MediaQuery.of(context).padding.top; return Stack( //alignment: const Alignment(1.2, 0.6), children: [ @@ -90,7 +91,7 @@ class Page extends StatelessWidget { ), Positioned( right: -5.0, - top: 2.0, + top: paddingTop + 2.0, child: creatButton(context, 'GitHub', Icons.arrow_forward, 'goGithub') ), ] diff --git a/lib/views/web_page/web_view_page.dart b/lib/views/web_page/web_view_page.dart index 8f39c7f6..fbadf772 100644 --- a/lib/views/web_page/web_view_page.dart +++ b/lib/views/web_page/web_view_page.dart @@ -76,6 +76,7 @@ class _WebViewPageState extends State { withZoom: false, withLocalStorage: true, withJavascript: true, + hidden: true, ), ); } From ba66f4a58b0337b3735e58f5905f828f302112b6 Mon Sep 17 00:00:00 2001 From: zymxxxs Date: Fri, 8 Nov 2019 01:57:36 +0800 Subject: [PATCH 3/3] feat: use package named zefyr online --- pubspec.yaml | 3 +- zefyr/.gitignore | 72 --- zefyr/CHANGELOG.md | 3 - zefyr/LICENSE | 1 - zefyr/README.md | 14 - zefyr/lib/src/fast_diff.dart | 40 -- zefyr/lib/src/widgets/buttons.dart | 583 --------------------- zefyr/lib/src/widgets/caret.dart | 42 -- zefyr/lib/src/widgets/code.dart | 47 -- zefyr/lib/src/widgets/common.dart | 155 ------ zefyr/lib/src/widgets/controller.dart | 177 ------- zefyr/lib/src/widgets/cursor_timer.dart | 42 -- zefyr/lib/src/widgets/editable_box.dart | 341 ------------ zefyr/lib/src/widgets/editable_text.dart | 283 ---------- zefyr/lib/src/widgets/editor.dart | 164 ------ zefyr/lib/src/widgets/field.dart | 86 --- zefyr/lib/src/widgets/horizontal_rule.dart | 121 ----- zefyr/lib/src/widgets/image.dart | 236 --------- zefyr/lib/src/widgets/input.dart | 198 ------- zefyr/lib/src/widgets/list.dart | 86 --- zefyr/lib/src/widgets/paragraph.dart | 68 --- zefyr/lib/src/widgets/quote.dart | 55 -- zefyr/lib/src/widgets/render_context.dart | 146 ------ zefyr/lib/src/widgets/rich_text.dart | 223 -------- zefyr/lib/src/widgets/scaffold.dart | 60 --- zefyr/lib/src/widgets/scope.dart | 232 -------- zefyr/lib/src/widgets/selection.dart | 512 ------------------ zefyr/lib/src/widgets/theme.dart | 312 ----------- zefyr/lib/src/widgets/toolbar.dart | 398 -------------- zefyr/lib/src/widgets/view.dart | 107 ---- zefyr/lib/util.dart | 40 -- zefyr/lib/zefyr.dart | 22 - zefyr/pubspec.yaml | 59 --- zefyr/test/zefyr_test.dart | 13 - 34 files changed, 1 insertion(+), 4940 deletions(-) delete mode 100644 zefyr/.gitignore delete mode 100644 zefyr/CHANGELOG.md delete mode 100644 zefyr/LICENSE delete mode 100644 zefyr/README.md delete mode 100644 zefyr/lib/src/fast_diff.dart delete mode 100644 zefyr/lib/src/widgets/buttons.dart delete mode 100644 zefyr/lib/src/widgets/caret.dart delete mode 100644 zefyr/lib/src/widgets/code.dart delete mode 100644 zefyr/lib/src/widgets/common.dart delete mode 100644 zefyr/lib/src/widgets/controller.dart delete mode 100644 zefyr/lib/src/widgets/cursor_timer.dart delete mode 100644 zefyr/lib/src/widgets/editable_box.dart delete mode 100644 zefyr/lib/src/widgets/editable_text.dart delete mode 100644 zefyr/lib/src/widgets/editor.dart delete mode 100644 zefyr/lib/src/widgets/field.dart delete mode 100644 zefyr/lib/src/widgets/horizontal_rule.dart delete mode 100644 zefyr/lib/src/widgets/image.dart delete mode 100644 zefyr/lib/src/widgets/input.dart delete mode 100644 zefyr/lib/src/widgets/list.dart delete mode 100644 zefyr/lib/src/widgets/paragraph.dart delete mode 100644 zefyr/lib/src/widgets/quote.dart delete mode 100644 zefyr/lib/src/widgets/render_context.dart delete mode 100644 zefyr/lib/src/widgets/rich_text.dart delete mode 100644 zefyr/lib/src/widgets/scaffold.dart delete mode 100644 zefyr/lib/src/widgets/scope.dart delete mode 100644 zefyr/lib/src/widgets/selection.dart delete mode 100644 zefyr/lib/src/widgets/theme.dart delete mode 100644 zefyr/lib/src/widgets/toolbar.dart delete mode 100644 zefyr/lib/src/widgets/view.dart delete mode 100644 zefyr/lib/util.dart delete mode 100644 zefyr/lib/zefyr.dart delete mode 100644 zefyr/pubspec.yaml delete mode 100644 zefyr/test/zefyr_test.dart diff --git a/pubspec.yaml b/pubspec.yaml index 20d335e3..862e0453 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,8 +51,7 @@ dependencies: open_file: ^2.0.1+2 package_info: ^0.4.0+3 flutter_jpush: ^0.0.4 - zefyr: - path: ./zefyr + zefyr: ^0.8.0 pull_to_refresh: ^1.5.6 diff --git a/zefyr/.gitignore b/zefyr/.gitignore deleted file mode 100644 index 9f87252c..00000000 --- a/zefyr/.gitignore +++ /dev/null @@ -1,72 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# 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/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.packages -.pub-cache/ -.pub/ -build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# 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/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# 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 diff --git a/zefyr/CHANGELOG.md b/zefyr/CHANGELOG.md deleted file mode 100644 index ac071598..00000000 --- a/zefyr/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## [0.0.1] - TODO: Add release date. - -* TODO: Describe initial release. diff --git a/zefyr/LICENSE b/zefyr/LICENSE deleted file mode 100644 index ba75c69f..00000000 --- a/zefyr/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/zefyr/README.md b/zefyr/README.md deleted file mode 100644 index cc0c4567..00000000 --- a/zefyr/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# zefyr - -A new Flutter package project. - -## Getting Started - -This project is a starting point for a Dart -[package](https://flutter.dev/developing-packages/), -a library module containing code that can be shared easily across -multiple Flutter or Dart projects. - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/zefyr/lib/src/fast_diff.dart b/zefyr/lib/src/fast_diff.dart deleted file mode 100644 index ade850b6..00000000 --- a/zefyr/lib/src/fast_diff.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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; - -/// Performs a fast diff operation on two input strings based on provided -/// [cursorPosition]. -DiffResult fastDiff(String oldText, String newText, int cursorPosition) { - var delta = newText.length - oldText.length; - var limit = math.max(0, cursorPosition - delta); - var end = oldText.length; - while (end > limit && oldText[end - 1] == newText[end + delta - 1]) { - end -= 1; - } - var start = 0; - var startLimit = cursorPosition - math.max(0, delta); - while (start < startLimit && oldText[start] == newText[start]) { - start += 1; - } - final String deleted = (start < end) ? oldText.substring(start, end) : ''; - final inserted = newText.substring(start, end + delta); - return new DiffResult(start, deleted, inserted); -} - -/// A diff between two strings of text. -class DiffResult { - /// Start index in old text at which changes begin. - final int start; - - /// Deleted text in old text. - final String deleted; - - /// Inserted text. - final String inserted; - - DiffResult(this.start, this.deleted, this.inserted); - - @override - String toString() => 'DiffResult[$start, "$deleted", "$inserted"]'; -} diff --git a/zefyr/lib/src/widgets/buttons.dart b/zefyr/lib/src/widgets/buttons.dart deleted file mode 100644 index 675f4ea3..00000000 --- a/zefyr/lib/src/widgets/buttons.dart +++ /dev/null @@ -1,583 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; -import 'package:flutter/services.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:notus/notus.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'scope.dart'; -import 'theme.dart'; -import 'toolbar.dart'; - -/// A button used in [ZefyrToolbar]. -/// -/// Create an instance of this widget with [ZefyrButton.icon] or -/// [ZefyrButton.text] constructors. -/// -/// Toolbar buttons are normally created by a [ZefyrToolbarDelegate]. -class ZefyrButton extends StatelessWidget { - /// Creates a toolbar button with an icon. - ZefyrButton.icon({ - @required this.action, - @required IconData icon, - double iconSize, - this.onPressed, - }) : assert(action != null), - assert(icon != null), - _icon = icon, - _iconSize = iconSize, - _text = null, - _textStyle = null, - super(); - - /// Creates a toolbar button containing text. - /// - /// Note that [ZefyrButton] has fixed width and does not expand to accommodate - /// long texts. - ZefyrButton.text({ - @required this.action, - @required String text, - TextStyle style, - this.onPressed, - }) : assert(action != null), - assert(text != null), - _icon = null, - _iconSize = null, - _text = text, - _textStyle = style, - super(); - - /// Toolbar action associated with this button. - final ZefyrToolbarAction action; - final IconData _icon; - final double _iconSize; - final String _text; - final TextStyle _textStyle; - - /// Callback to trigger when this button is tapped. - final VoidCallback onPressed; - - bool get isAttributeAction { - return kZefyrToolbarAttributeActions.keys.contains(action); - } - - @override - Widget build(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - final editor = toolbar.editor; - final toolbarTheme = ZefyrTheme.of(context).toolbarTheme; - final pressedHandler = _getPressedHandler(editor, toolbar); - final iconColor = (pressedHandler == null) - ? toolbarTheme.disabledIconColor - : toolbarTheme.iconColor; - if (_icon != null) { - return RawZefyrButton.icon( - action: action, - icon: _icon, - size: _iconSize, - iconColor: iconColor, - color: _getColor(editor, toolbarTheme), - onPressed: _getPressedHandler(editor, toolbar), - ); - } else { - assert(_text != null); - var style = _textStyle ?? new TextStyle(); - style = style.copyWith(color: iconColor); - return RawZefyrButton( - action: action, - child: new Text(_text, style: style), - color: _getColor(editor, toolbarTheme), - onPressed: _getPressedHandler(editor, toolbar), - ); - } - } - - Color _getColor(ZefyrScope editor, ZefyrToolbarTheme theme) { - if (isAttributeAction) { - final attribute = kZefyrToolbarAttributeActions[action]; - final isToggled = (attribute is NotusAttribute) - ? editor.selectionStyle.containsSame(attribute) - : editor.selectionStyle.contains(attribute); - return isToggled ? theme.toggleColor : null; - } - return null; - } - - VoidCallback _getPressedHandler( - ZefyrScope editor, ZefyrToolbarState toolbar) { - if (onPressed != null) { - return onPressed; - } else if (isAttributeAction) { - final attribute = kZefyrToolbarAttributeActions[action]; - if (attribute is NotusAttribute) { - return () => _toggleAttribute(attribute, editor); - } - } else if (action == ZefyrToolbarAction.close) { - return () => toolbar.closeOverlay(); - } else if (action == ZefyrToolbarAction.hideKeyboard) { - return () => editor.hideKeyboard(); - } - - return null; - } - - void _toggleAttribute(NotusAttribute attribute, ZefyrScope editor) { - final isToggled = editor.selectionStyle.containsSame(attribute); - if (isToggled) { - editor.formatSelection(attribute.unset); - } else { - editor.formatSelection(attribute); - } - } -} - -/// Raw button widget used by [ZefyrToolbar]. -/// -/// See also: -/// -/// * [ZefyrButton], which wraps this widget and implements most of the -/// action-specific logic. -class RawZefyrButton extends StatelessWidget { - const RawZefyrButton({ - Key key, - @required this.action, - @required this.child, - @required this.color, - @required this.onPressed, - }) : super(key: key); - - /// Creates a [RawZefyrButton] containing an icon. - RawZefyrButton.icon({ - @required this.action, - @required IconData icon, - double size, - Color iconColor, - @required this.color, - @required this.onPressed, - }) : child = new Icon(icon, size: size, color: iconColor), - super(); - - /// Toolbar action associated with this button. - final ZefyrToolbarAction action; - - /// Child widget to show inside this button. Usually an icon. - final Widget child; - - /// Background color of this button. - final Color color; - - /// Callback to trigger when this button is pressed. - final VoidCallback onPressed; - - /// Returns `true` if this button is currently toggled on. - bool get isToggled => color != null; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final width = theme.buttonTheme.constraints.minHeight + 4.0; - final constraints = theme.buttonTheme.constraints.copyWith( - minWidth: width, maxHeight: theme.buttonTheme.constraints.minHeight); - final radius = BorderRadius.all(Radius.circular(3.0)); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 6.0), - child: RawMaterialButton( - shape: RoundedRectangleBorder(borderRadius: radius), - elevation: 0.0, - fillColor: color, - constraints: constraints, - onPressed: onPressed, - child: child, - ), - ); - } -} - -/// Controls heading styles. -/// -/// When pressed, this button displays overlay toolbar with three -/// buttons for each heading level. -class HeadingButton extends StatefulWidget { - const HeadingButton({Key key}) : super(key: key); - - @override - _HeadingButtonState createState() => _HeadingButtonState(); -} - -class _HeadingButtonState extends State { - @override - Widget build(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - return toolbar.buildButton( - context, - ZefyrToolbarAction.heading, - onPressed: showOverlay, - ); - } - - void showOverlay() { - final toolbar = ZefyrToolbar.of(context); - toolbar.showOverlay(buildOverlay); - } - - Widget buildOverlay(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - final buttons = Row( - children: [ - SizedBox(width: 8.0), - toolbar.buildButton(context, ZefyrToolbarAction.headingLevel1), - toolbar.buildButton(context, ZefyrToolbarAction.headingLevel2), - toolbar.buildButton(context, ZefyrToolbarAction.headingLevel3), - ], - ); - return ZefyrToolbarScaffold(body: buttons); - } -} - -/// Controls image attribute. -/// -/// When pressed, this button displays overlay toolbar with three -/// buttons for each heading level. -class ImageButton extends StatefulWidget { - const ImageButton({Key key}) : super(key: key); - - @override - _ImageButtonState createState() => _ImageButtonState(); -} - -class _ImageButtonState extends State { - @override - Widget build(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - return toolbar.buildButton( - context, - ZefyrToolbarAction.image, - onPressed: showOverlay, - ); - } - - void showOverlay() { - final toolbar = ZefyrToolbar.of(context); - toolbar.showOverlay(buildOverlay); - } - - Widget buildOverlay(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - final buttons = Row( - children: [ - SizedBox(width: 8.0), - toolbar.buildButton(context, ZefyrToolbarAction.cameraImage, - onPressed: _pickFromCamera), - toolbar.buildButton(context, ZefyrToolbarAction.galleryImage, - onPressed: _pickFromGallery), - ], - ); - return ZefyrToolbarScaffold(body: buttons); - } - - void _pickFromCamera() async { - final editor = ZefyrToolbar.of(context).editor; - final image = await editor.imageDelegate.pickImage(ImageSource.camera); - if (image != null) - editor.formatSelection(NotusAttribute.embed.image(image)); - } - - void _pickFromGallery() async { - final editor = ZefyrToolbar.of(context).editor; - final image = await editor.imageDelegate.pickImage(ImageSource.gallery); - if (image != null) - editor.formatSelection(NotusAttribute.embed.image(image)); - } -} - -class LinkButton extends StatefulWidget { - const LinkButton({Key key}) : super(key: key); - - @override - _LinkButtonState createState() => _LinkButtonState(); -} - -class _LinkButtonState extends State { - final TextEditingController _inputController = TextEditingController(); - Key _inputKey; - bool _formatError = false; - ZefyrScope _editor; - - bool get isEditing => _inputKey != null; - - @override - Widget build(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - final editor = toolbar.editor; - final enabled = - hasLink(editor.selectionStyle) || !editor.selection.isCollapsed; - - return toolbar.buildButton( - context, - ZefyrToolbarAction.link, - onPressed: enabled ? showOverlay : null, - ); - } - - bool hasLink(NotusStyle style) => style.contains(NotusAttribute.link); - - String getLink([String defaultValue]) { - final editor = ZefyrToolbar.of(context).editor; - final attrs = editor.selectionStyle; - if (hasLink(attrs)) { - return attrs.value(NotusAttribute.link); - } - return defaultValue; - } - - void showOverlay() { - final toolbar = ZefyrToolbar.of(context); - toolbar.showOverlay(buildOverlay).whenComplete(cancelEdit); - } - - void closeOverlay() { - final toolbar = ZefyrToolbar.of(context); - toolbar.closeOverlay(); - } - - void edit() { - final toolbar = ZefyrToolbar.of(context); - setState(() { - _inputKey = new UniqueKey(); - _inputController.text = getLink('https://'); - _inputController.addListener(_handleInputChange); - toolbar.markNeedsRebuild(); - }); - } - - void doneEdit() { - final toolbar = ZefyrToolbar.of(context); - setState(() { - var error = false; - if (_inputController.text.isNotEmpty) { - try { - var uri = Uri.parse(_inputController.text); - if ((uri.isScheme('https') || uri.isScheme('http')) && - uri.host.isNotEmpty) { - toolbar.editor.formatSelection( - NotusAttribute.link.fromString(_inputController.text)); - } else { - error = true; - } - } on FormatException { - error = true; - } - } - if (error) { - _formatError = error; - toolbar.markNeedsRebuild(); - } else { - _inputKey = null; - _inputController.text = ''; - _inputController.removeListener(_handleInputChange); - toolbar.markNeedsRebuild(); - toolbar.editor.focus(); - } - }); - } - - void cancelEdit() { - if (mounted) { - final editor = ZefyrToolbar.of(context).editor; - setState(() { - _inputKey = null; - _inputController.text = ''; - _inputController.removeListener(_handleInputChange); - editor.focus(); - }); - } - } - - void unlink() { - final editor = ZefyrToolbar.of(context).editor; - editor.formatSelection(NotusAttribute.link.unset); - closeOverlay(); - } - - void copyToClipboard() { - var link = getLink(); - assert(link != null); - Clipboard.setData(new ClipboardData(text: link)); - } - - void openInBrowser() async { - final editor = ZefyrToolbar.of(context).editor; - var link = getLink(); - assert(link != null); - if (await canLaunch(link)) { - editor.hideKeyboard(); - await launch(link, forceWebView: true); - } - } - - void _handleInputChange() { - final toolbar = ZefyrToolbar.of(context); - setState(() { - _formatError = false; - toolbar.markNeedsRebuild(); - }); - } - - Widget buildOverlay(BuildContext context) { - final toolbar = ZefyrToolbar.of(context); - final style = toolbar.editor.selectionStyle; - - String value = 'Tap to edit link'; - if (style.contains(NotusAttribute.link)) { - value = style.value(NotusAttribute.link); - } - final clipboardEnabled = value != 'Tap to edit link'; - final body = !isEditing - ? _LinkView(value: value, onTap: edit) - : _LinkInput( - key: _inputKey, - controller: _inputController, - formatError: _formatError, - ); - final items = [Expanded(child: body)]; - if (!isEditing) { - final unlinkHandler = hasLink(style) ? unlink : null; - final copyHandler = clipboardEnabled ? copyToClipboard : null; - final openHandler = hasLink(style) ? openInBrowser : null; - final buttons = [ - toolbar.buildButton(context, ZefyrToolbarAction.unlink, - onPressed: unlinkHandler), - toolbar.buildButton(context, ZefyrToolbarAction.clipboardCopy, - onPressed: copyHandler), - toolbar.buildButton( - context, - ZefyrToolbarAction.openInBrowser, - onPressed: openHandler, - ), - ]; - items.addAll(buttons); - } - final trailingPressed = isEditing ? doneEdit : closeOverlay; - final trailingAction = - isEditing ? ZefyrToolbarAction.confirm : ZefyrToolbarAction.close; - - return ZefyrToolbarScaffold( - body: Row(children: items), - trailing: toolbar.buildButton( - context, - trailingAction, - onPressed: trailingPressed, - ), - ); - } -} - -class _LinkInput extends StatefulWidget { - final TextEditingController controller; - final bool formatError; - - const _LinkInput( - {Key key, @required this.controller, this.formatError: false}) - : super(key: key); - - @override - _LinkInputState createState() { - return new _LinkInputState(); - } -} - -class _LinkInputState extends State<_LinkInput> { - final FocusNode _focusNode = FocusNode(); - - ZefyrScope _editor; - bool _didAutoFocus = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!_didAutoFocus) { - FocusScope.of(context).requestFocus(_focusNode); - _didAutoFocus = true; - } - - final toolbar = ZefyrToolbar.of(context); - - if (_editor != toolbar.editor) { - _editor?.toolbarFocusNode = null; - _editor = toolbar.editor; - _editor.toolbarFocusNode = _focusNode; - } - } - - @override - void dispose() { - _editor?.toolbarFocusNode = null; - _focusNode.dispose(); - _editor = null; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final toolbarTheme = ZefyrTheme.of(context).toolbarTheme; - final color = - widget.formatError ? Colors.redAccent : toolbarTheme.iconColor; - final style = theme.textTheme.subhead.copyWith(color: color); - return TextField( - style: style, - keyboardType: TextInputType.url, - focusNode: _focusNode, - controller: widget.controller, - autofocus: true, - decoration: new InputDecoration( - hintText: 'https://', - filled: true, - fillColor: toolbarTheme.color, - border: InputBorder.none, - contentPadding: const EdgeInsets.all(10.0), - ), - ); - } -} - -class _LinkView extends StatelessWidget { - const _LinkView({Key key, @required this.value, this.onTap}) - : super(key: key); - final String value; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final toolbarTheme = ZefyrTheme.of(context).toolbarTheme; - Widget widget = new ClipRect( - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: ZefyrToolbar.kToolbarHeight), - padding: const EdgeInsets.all(10.0), - child: Text( - value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.subhead - .copyWith(color: toolbarTheme.disabledIconColor), - ), - ) - ], - ), - ); - if (onTap != null) { - widget = GestureDetector( - child: widget, - onTap: onTap, - ); - } - return widget; - } -} diff --git a/zefyr/lib/src/widgets/caret.dart b/zefyr/lib/src/widgets/caret.dart deleted file mode 100644 index 895cd3b4..00000000 --- a/zefyr/lib/src/widgets/caret.dart +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; - -import 'package:flutter/material.dart'; - -/// Helper class responsible for cursor layout and painting. -class CursorPainter { - static const double _kCaretHeightOffset = 2.0; // pixels - static const double _kCaretWidth = 1.0; // pixels - - static Rect buildPrototype(double lineHeight) { - return new Rect.fromLTWH( - 0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset); - } - - CursorPainter(Color color) - : assert(color != null), - _color = color; - - Rect _prototype; - - Rect get prototype => _prototype; - - Color _color; - Color get color => _color; - set color(Color value) { - assert(value != null); - _color = value; - } - - void layout(double lineHeight) { - _prototype = buildPrototype(lineHeight); - } - - void paint(Canvas canvas, Offset offset) { - final Paint paint = new Paint()..color = _color; - final Rect caretRect = _prototype.shift(offset); - canvas.drawRect(caretRect, paint); - } -} diff --git a/zefyr/lib/src/widgets/code.dart b/zefyr/lib/src/widgets/code.dart deleted file mode 100644 index 1c5c6050..00000000 --- a/zefyr/lib/src/widgets/code.dart +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; -import 'package:notus/notus.dart'; - -import 'common.dart'; -import 'theme.dart'; - -/// Represents a code snippet in Zefyr editor. -class ZefyrCode extends StatelessWidget { - const ZefyrCode({Key key, @required this.node}) : super(key: key); - - /// Document node represented by this widget. - final BlockNode node; - - @override - Widget build(BuildContext context) { - final theme = ZefyrTheme.of(context); - - List items = []; - for (var line in node.children) { - items.add(_buildLine(line, theme.blockTheme.code.textStyle)); - } - - return new Padding( - padding: theme.blockTheme.code.padding, - child: new Container( - // TODO: make decorations configurable - decoration: BoxDecoration( - color: Colors.blueGrey.shade50, - borderRadius: BorderRadius.circular(3.0), - ), - padding: const EdgeInsets.all(16.0), - child: new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: items, - ), - ), - ); - } - - Widget _buildLine(Node node, TextStyle style) { - LineNode line = node; - return new RawZefyrLine(node: line, style: style); - } -} diff --git a/zefyr/lib/src/widgets/common.dart b/zefyr/lib/src/widgets/common.dart deleted file mode 100644 index aabc8b8f..00000000 --- a/zefyr/lib/src/widgets/common.dart +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:notus/notus.dart'; - -import 'editable_box.dart'; -import 'horizontal_rule.dart'; -import 'image.dart'; -import 'rich_text.dart'; -import 'scope.dart'; -import 'theme.dart'; - -/// Raw widget representing a single line of rich text document in Zefyr editor. -/// -/// See [ZefyrParagraph] and [ZefyrHeading] which wrap this widget and -/// integrate it with current [ZefyrTheme]. -class RawZefyrLine extends StatefulWidget { - const RawZefyrLine({ - Key key, - @required this.node, - this.style, - this.padding, - }) : super(key: key); - - /// Line in the document represented by this widget. - final LineNode node; - - /// Style to apply to this line. Required for lines with text contents, - /// ignored for lines containing embeds. - final TextStyle style; - - /// Padding to add around this paragraph. - final EdgeInsets padding; - - @override - _RawZefyrLineState createState() => new _RawZefyrLineState(); -} - -class _RawZefyrLineState extends State { - final LayerLink _link = new LayerLink(); - - @override - Widget build(BuildContext context) { - final scope = ZefyrScope.of(context); - if (scope.isEditable) { - ensureVisible(context, scope); - } - final theme = ZefyrTheme.of(context); - - Widget content; - if (widget.node.hasEmbed) { - content = buildEmbed(context, scope); - } else { - assert(widget.style != null); - content = ZefyrRichText( - node: widget.node, - text: buildText(context), - ); - } - - if (scope.isEditable) { - content = EditableBox( - child: content, - node: widget.node, - layerLink: _link, - renderContext: scope.renderContext, - showCursor: scope.showCursor, - selection: scope.selection, - selectionColor: theme.selectionColor, - cursorColor: theme.cursorColor, - ); - content = CompositedTransformTarget(link: _link, child: content); - } - - if (widget.padding != null) { - return Padding(padding: widget.padding, child: content); - } - return content; - } - - void ensureVisible(BuildContext context, ZefyrScope scope) { - if (scope.selection.isCollapsed && - widget.node.containsOffset(scope.selection.extentOffset)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - bringIntoView(context); - }); - } - } - - void bringIntoView(BuildContext context) { - ScrollableState scrollable = Scrollable.of(context); - final object = context.findRenderObject(); - assert(object.attached); - final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); - assert(viewport != null); - - final double offset = scrollable.position.pixels; - double target = viewport.getOffsetToReveal(object, 0.0).offset; - if (target - offset < 0.0) { - scrollable.position.jumpTo(target); - return; - } - target = viewport.getOffsetToReveal(object, 1.0).offset; - if (target - offset > 0.0) { - scrollable.position.jumpTo(target); - } - } - - TextSpan buildText(BuildContext context) { - final theme = ZefyrTheme.of(context); - final List children = widget.node.children - .map((node) => _segmentToTextSpan(node, theme)) - .toList(growable: false); - return new TextSpan(style: widget.style, children: children); - } - - TextSpan _segmentToTextSpan(Node node, ZefyrThemeData theme) { - final TextNode segment = node; - final attrs = segment.style; - - return new TextSpan( - text: segment.value, - style: _getTextStyle(attrs, theme), - ); - } - - TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) { - TextStyle result = new TextStyle(); - if (style.containsSame(NotusAttribute.bold)) { - result = result.merge(theme.boldStyle); - } - if (style.containsSame(NotusAttribute.italic)) { - result = result.merge(theme.italicStyle); - } - if (style.contains(NotusAttribute.link)) { - result = result.merge(theme.linkStyle); - } - return result; - } - - Widget buildEmbed(BuildContext context, ZefyrScope scope) { - EmbedNode node = widget.node.children.single; - EmbedAttribute embed = node.style.get(NotusAttribute.embed); - - if (embed.type == EmbedType.horizontalRule) { - return ZefyrHorizontalRule(node: node); - } else if (embed.type == EmbedType.image) { - return ZefyrImage(node: node, delegate: scope.imageDelegate); - } else { - throw new UnimplementedError('Unimplemented embed type ${embed.type}'); - } - } -} diff --git a/zefyr/lib/src/widgets/controller.dart b/zefyr/lib/src/widgets/controller.dart deleted file mode 100644 index 325769d3..00000000 --- a/zefyr/lib/src/widgets/controller.dart +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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/widgets.dart'; -import 'package:notus/notus.dart'; -import 'package:quill_delta/quill_delta.dart'; -import 'package:zefyr/util.dart'; - -const TextSelection _kZeroSelection = const TextSelection.collapsed( - offset: 0, - affinity: TextAffinity.upstream, -); - -/// Owner of focus. -enum FocusOwner { - /// Current owner is the editor. - editor, - - /// Current owner is the toolbar. - toolbar, - - /// No focus owner. - none, -} - -/// Controls instance of [ZefyrEditor]. -class ZefyrController extends ChangeNotifier { - ZefyrController(NotusDocument document) - : assert(document != null), - _document = document; - - /// Zefyr document managed by this controller. - NotusDocument get document => _document; - NotusDocument _document; - - /// Currently selected text within the [document]. - TextSelection get selection => _selection; - TextSelection _selection = _kZeroSelection; - - ChangeSource _lastChangeSource; - - /// Source of the last text or selection change. - ChangeSource get lastChangeSource => _lastChangeSource; - - /// Updates selection with specified [value]. - /// - /// [value] and [source] cannot be `null`. - void updateSelection(TextSelection value, - {ChangeSource source: ChangeSource.remote}) { - _updateSelectionSilent(value, source: source); - notifyListeners(); - } - - // Updates selection without triggering notifications to listeners. - void _updateSelectionSilent(TextSelection value, - {ChangeSource source: ChangeSource.remote}) { - assert(value != null && source != null); - _selection = value; - _lastChangeSource = source; - _ensureSelectionBeforeLastBreak(); - } - - @override - void dispose() { - _document.close(); - super.dispose(); - } - - /// Composes [change] into document managed by this controller. - /// - /// This method does not apply any adjustments or heuristic rules to - /// provided [change] and it is caller's responsibility to ensure this change - /// can be composed without errors. - /// - /// If composing this change fails then this method throws [ComposeError]. - void compose(Delta change, - {TextSelection selection, ChangeSource source: ChangeSource.remote}) { - if (change.isNotEmpty) { - _document.compose(change, source); - } - if (selection != null) { - _updateSelectionSilent(selection, source: source); - } else { - // Transform selection against the composed change and give priority to - // current position (force: false). - final base = - change.transformPosition(_selection.baseOffset, force: false); - final extent = - change.transformPosition(_selection.extentOffset, force: false); - selection = _selection.copyWith(baseOffset: base, extentOffset: extent); - if (_selection != selection) { - _updateSelectionSilent(selection, source: source); - } - } - _lastChangeSource = source; - notifyListeners(); - } - - void replaceText(int index, int length, String text, - {TextSelection selection}) { - Delta delta; - - if (length > 0 || text.isNotEmpty) { - delta = document.replace(index, length, text); - } - - if (selection != null) { - if (delta == null) { - _updateSelectionSilent(selection, source: ChangeSource.local); - } else { - // need to transform selection position in case actual delta - // is different from user's version (in deletes and inserts). - Delta user = new Delta() - ..retain(index) - ..insert(text) - ..delete(length); - int positionDelta = getPositionDelta(user, delta); - _updateSelectionSilent( - selection.copyWith( - baseOffset: selection.baseOffset + positionDelta, - extentOffset: selection.extentOffset + positionDelta, - ), - source: ChangeSource.local, - ); - } - } - _lastChangeSource = ChangeSource.local; - notifyListeners(); - } - - void formatText(int index, int length, NotusAttribute attribute) { - final change = document.format(index, length, attribute); - _lastChangeSource = ChangeSource.local; - // Transform selection against the composed change and give priority to - // the change. This is needed in cases when format operation actually - // inserts data into the document (e.g. embeds). - final base = change.transformPosition(_selection.baseOffset); - final extent = - change.transformPosition(_selection.extentOffset); - final adjustedSelection = - _selection.copyWith(baseOffset: base, extentOffset: extent); - if (_selection != adjustedSelection) { - _updateSelectionSilent(adjustedSelection, source: _lastChangeSource); - } - notifyListeners(); - } - - /// Formats current selection with [attribute]. - void formatSelection(NotusAttribute attribute) { - int index = _selection.start; - int length = _selection.end - index; - formatText(index, length, attribute); - } - - NotusStyle getSelectionStyle() { - int start = _selection.start; - int length = _selection.end - start; - return _document.collectStyle(start, length); - } - - TextEditingValue get plainTextEditingValue { - return new TextEditingValue( - text: document.toPlainText(), - selection: selection, - composing: new TextRange.collapsed(0), - ); - } - - void _ensureSelectionBeforeLastBreak() { - final end = _document.length - 1; - final base = math.min(_selection.baseOffset, end); - final extent = math.min(_selection.extentOffset, end); - _selection = _selection.copyWith(baseOffset: base, extentOffset: extent); - } -} diff --git a/zefyr/lib/src/widgets/cursor_timer.dart b/zefyr/lib/src/widgets/cursor_timer.dart deleted file mode 100644 index 947dc5ab..00000000 --- a/zefyr/lib/src/widgets/cursor_timer.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Helper class that keeps state relevant to the editing cursor. -class CursorTimer { - static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500); - - Timer _timer; - final ValueNotifier _showCursor = new ValueNotifier(false); - - ValueNotifier get value => _showCursor; - - void _cursorTick(Timer timer) { - _showCursor.value = !_showCursor.value; - } - - /// Starts cursor timer. - void start() { - _showCursor.value = true; - _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); - } - - /// Stops cursor timer. - void stop() { - _timer?.cancel(); - _timer = null; - _showCursor.value = false; - } - - /// Starts or stops cursor timer based on current state of [focusNode] - /// and [selection]. - void startOrStop(FocusNode focusNode, TextSelection selection) { - final hasFocus = focusNode.hasFocus; - final selectionCollapsed = selection.isCollapsed; - if (_timer == null && hasFocus && selectionCollapsed) { - start(); - } else if (_timer != null && (!hasFocus || !selectionCollapsed)) { - stop(); - } - } -} diff --git a/zefyr/lib/src/widgets/editable_box.dart b/zefyr/lib/src/widgets/editable_box.dart deleted file mode 100644 index 71c52f48..00000000 --- a/zefyr/lib/src/widgets/editable_box.dart +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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' as ui; - -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:notus/notus.dart'; - -import 'caret.dart'; -import 'render_context.dart'; - -class EditableBox extends SingleChildRenderObjectWidget { - EditableBox({ - @required Widget child, - @required this.node, - @required this.layerLink, - @required this.renderContext, - @required this.showCursor, - @required this.selection, - @required this.selectionColor, - @required this.cursorColor, - }) : super(child: child); - - final ContainerNode node; - final LayerLink layerLink; - final ZefyrRenderContext renderContext; - final ValueNotifier showCursor; - final TextSelection selection; - final Color selectionColor; - final Color cursorColor; - - @override - RenderEditableProxyBox createRenderObject(BuildContext context) { - return new RenderEditableProxyBox( - node: node, - layerLink: layerLink, - renderContext: renderContext, - showCursor: showCursor, - selection: selection, - selectionColor: selectionColor, - cursorColor: cursorColor, - ); - } - - @override - void updateRenderObject( - BuildContext context, RenderEditableProxyBox renderObject) { - renderObject - ..node = node - ..layerLink = layerLink - ..renderContext = renderContext - ..showCursor = showCursor - ..selection = selection - ..selectionColor = selectionColor - ..cursorColor = cursorColor; - } -} - -class RenderEditableProxyBox extends RenderBox - with - RenderObjectWithChildMixin, - RenderProxyBoxMixin - implements RenderEditableBox { - RenderEditableProxyBox({ - RenderEditableBox child, - @required ContainerNode node, - @required LayerLink layerLink, - @required ZefyrRenderContext renderContext, - @required ValueNotifier showCursor, - @required TextSelection selection, - @required Color selectionColor, - @required Color cursorColor, - }) : _node = node, - _layerLink = layerLink, - _renderContext = renderContext, - _showCursor = showCursor, - _selection = selection, - _selectionColor = selectionColor, - super() { - this.child = child; - _cursorPainter = CursorPainter(cursorColor); - } - - CursorPainter _cursorPainter; - - set cursorColor(Color value) { - if (_cursorPainter.color != value) { - _cursorPainter.color = value; - markNeedsPaint(); - } - } - - bool _isDirty = true; - - ContainerNode get node => _node; - ContainerNode _node; - void set node(ContainerNode value) { - _node = value; - } - - LayerLink get layerLink => _layerLink; - LayerLink _layerLink; - void set layerLink(LayerLink value) { - if (_layerLink == value) return; - _layerLink = value; - } - - ZefyrRenderContext _renderContext; - void set renderContext(ZefyrRenderContext value) { - if (_renderContext == value) return; - if (attached) _renderContext.removeBox(this); - _renderContext = value; - if (attached) _renderContext.addBox(this); - } - - ValueNotifier _showCursor; - set showCursor(ValueNotifier value) { - assert(value != null); - if (_showCursor == value) return; - if (attached) _showCursor.removeListener(markNeedsCursorPaint); - _showCursor = value; - if (attached) _showCursor.addListener(markNeedsCursorPaint); - markNeedsPaint(); - } - - /// Current document selection. - TextSelection get selection => _selection; - TextSelection _selection; - set selection(TextSelection value) { - if (_selection == value) return; - // TODO: check if selection affects this block (also check previous value) - _selection = value; - markNeedsPaint(); - } - - /// Color of selection. - Color get selectionColor => _selectionColor; - Color _selectionColor; - set selectionColor(Color value) { - if (_selectionColor == value) return; - _selectionColor = value; - markNeedsPaint(); - } - - /// Returns `true` if current selection is collapsed, located within - /// this paragraph and is visible according to tick timer. - bool get isCaretVisible { - return _showCursor.value && containsCaret; - } - - /// Returns `true` if current selection is collapsed and located - /// within this paragraph. - bool get containsCaret { - if (!_selection.isCollapsed) return false; - - final int start = node.documentOffset; - final int end = start + node.length; - final int caretOffset = _selection.extentOffset; - return caretOffset >= start && caretOffset < end; - } - - /// Returns `true` if selection is not collapsed and intersects with this - /// paragraph. - bool get isSelectionVisible { - if (_selection.isCollapsed) return false; - return intersectsWithSelection(_selection); - } - - void markNeedsCursorPaint() { - if (containsCaret) { - markNeedsPaint(); - } - } - - // - // Overridden members of RenderBox - // - - @override - void attach(PipelineOwner owner) { - super.attach(owner); - _showCursor.addListener(markNeedsCursorPaint); - _renderContext.addBox(this); - _renderContext.markDirty(this, _isDirty); - } - - @override - void detach() { - _showCursor.removeListener(markNeedsCursorPaint); - _renderContext.removeBox(this); - super.detach(); - } - - @override - @mustCallSuper - void performLayout() { - super.performLayout(); - _cursorPainter.layout(preferredLineHeight); - // Indicate to render context that this object can be used by other - // layers (selection overlay, for instance). - _isDirty = false; - _renderContext.markDirty(this, false); - } - - @override - void markNeedsLayout() { - // Temporarily remove this object from the render context. - _isDirty = true; - _renderContext.markDirty(this, true); - super.markNeedsLayout(); - } - - @override - void paint(PaintingContext context, Offset offset) { - if (selectionOrder == SelectionOrder.background && isSelectionVisible) { - paintSelection(context, offset, selection, selectionColor); - } - super.paint(context, offset); - if (selectionOrder == SelectionOrder.foreground && isSelectionVisible) { - paintSelection(context, offset, selection, selectionColor); - } - if (isCaretVisible) { - _paintCursor(context, offset); - } - } - - void _paintCursor(PaintingContext context, Offset offset) { - Offset caretOffset = - getOffsetForCaret(_selection.extent, _cursorPainter.prototype); - _cursorPainter.paint(context.canvas, caretOffset + offset); - } - - @override - bool hitTestSelf(Offset position) => true; - - @override - bool hitTest(HitTestResult result, {Offset position}) { - if (size.contains(position)) { - result.add(new BoxHitTestEntry(this, position)); - return true; - } - return false; - } - - // - // Proxy methods - // - - @override - double get preferredLineHeight => child.preferredLineHeight; - - @override - SelectionOrder get selectionOrder => child.selectionOrder; - - @override - void paintSelection(PaintingContext context, Offset offset, - TextSelection selection, Color selectionColor) => - child.paintSelection(context, offset, selection, selectionColor); - - @override - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) => - child.getOffsetForCaret(position, caretPrototype); - - @override - TextSelection getLocalSelection(TextSelection documentSelection) => - child.getLocalSelection(documentSelection); - - bool intersectsWithSelection(TextSelection selection) => - child.intersectsWithSelection(selection); - - @override - List getEndpointsForSelection(TextSelection selection) => - child.getEndpointsForSelection(selection); - - @override - ui.TextPosition getPositionForOffset(ui.Offset offset) => - child.getPositionForOffset(offset); - - @override - TextRange getWordBoundary(ui.TextPosition position) => - child.getWordBoundary(position); -} - -enum SelectionOrder { - /// Background selection is painted before primary content of editable box. - background, - - /// Foreground selection is painted after primary content of editable box. - foreground, -} - -abstract class RenderEditableBox extends RenderBox { - Node get node; - double get preferredLineHeight; - - TextPosition getPositionForOffset(Offset offset); - List getEndpointsForSelection(TextSelection selection); - - /// 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); - - /// Paint order of selection in this editable box. - SelectionOrder get selectionOrder; - - void paintSelection(PaintingContext context, Offset offset, - TextSelection selection, Color selectionColor); - - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype); - - /// Returns part of [documentSelection] local to this box. May return - /// `null`. - /// - /// [documentSelection] must not be collapsed. - TextSelection getLocalSelection(TextSelection documentSelection) { - if (!intersectsWithSelection(documentSelection)) return null; - - int nodeBase = node.documentOffset; - int nodeExtent = nodeBase + node.length; - int base = math.max(0, documentSelection.baseOffset - nodeBase); - int extent = - math.min(documentSelection.extentOffset, nodeExtent) - nodeBase; - return documentSelection.copyWith(baseOffset: base, extentOffset: extent); - } - - /// Returns `true` if this box intersects with document [selection]. - bool intersectsWithSelection(TextSelection selection) { - final int base = node.documentOffset; - final int extent = base + node.length; - return base <= selection.extentOffset && selection.baseOffset <= extent; - } -} diff --git a/zefyr/lib/src/widgets/editable_text.dart b/zefyr/lib/src/widgets/editable_text.dart deleted file mode 100644 index 37fdcf41..00000000 --- a/zefyr/lib/src/widgets/editable_text.dart +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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/cupertino.dart'; -import 'package:flutter/widgets.dart'; -import 'package:notus/notus.dart'; - -import 'code.dart'; -import 'common.dart'; -import 'controller.dart'; -import 'cursor_timer.dart'; -import 'editor.dart'; -import 'image.dart'; -import 'input.dart'; -import 'list.dart'; -import 'paragraph.dart'; -import 'quote.dart'; -import 'render_context.dart'; -import 'scope.dart'; -import 'selection.dart'; -import 'theme.dart'; - -/// Core widget responsible for editing Zefyr documents. -/// -/// Depends on presence of [ZefyrTheme] and [ZefyrScope] somewhere up the -/// widget tree. -/// -/// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to -/// edit style attributes. -class ZefyrEditableText extends StatefulWidget { - const ZefyrEditableText({ - Key key, - @required this.controller, - @required this.focusNode, - @required this.imageDelegate, - this.autofocus: true, - this.enabled: true, - this.padding: const EdgeInsets.symmetric(horizontal: 16.0), - this.physics, - }) : super(key: key); - - final ZefyrController controller; - final FocusNode focusNode; - final ZefyrImageDelegate imageDelegate; - final bool autofocus; - final bool enabled; - final ScrollPhysics physics; - - /// Padding around editable area. - final EdgeInsets padding; - - - - @override - _ZefyrEditableTextState createState() => new _ZefyrEditableTextState(); -} - -class _ZefyrEditableTextState extends State - with AutomaticKeepAliveClientMixin { - // - // New public members - // - - /// Focus node of this widget. - FocusNode get focusNode => widget.focusNode; - - /// Document controlled by this widget. - NotusDocument get document => widget.controller.document; - - /// Current text selection. - TextSelection get selection => widget.controller.selection; - - /// 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 (focusNode.hasFocus) - _input.openConnection(widget.controller.plainTextEditingValue); - else - FocusScope.of(context).requestFocus(focusNode); - } - - void focusOrUnfocusIfNeeded() { - if (!_didAutoFocus && widget.autofocus && widget.enabled) { - FocusScope.of(context).autofocus(focusNode); - _didAutoFocus = true; - } - if (!widget.enabled && focusNode.hasFocus) { - _didAutoFocus = false; - focusNode.unfocus(); - } - } - - // - // Overridden members of State - // - - - - @override - Widget build(BuildContext context) { -// var reparentIfNeeded = FocusScope.of(context).reparentIfNeeded(focusNode); - - _nodeAttachment.reparent(); - super.build(context); // See AutomaticKeepAliveState. - - Widget body = ListBody(children: _buildChildren(context)); - if (widget.padding != null) { - body = new Padding(padding: widget.padding, child: body); - } - final scrollable = SingleChildScrollView( - physics: widget.physics, - controller: _scrollController, - child: body, - ); - - final overlay = Overlay.of(context, debugRequiredFor: widget); - final layers = [scrollable]; - if (widget.enabled) { - layers.add(ZefyrSelectionOverlay( - controller: widget.controller, - controls: cupertinoTextSelectionControls, - overlay: overlay, - )); - } - - return Stack(fit: StackFit.expand, children: layers); - } - - FocusAttachment _nodeAttachment; - @override - void initState() { - super.initState(); -// FocusScopeNode _node = focusNode; - _nodeAttachment = focusNode.attach(context); - _input = new InputConnectionController(_handleRemoteValueChange); - _updateSubscriptions(); - } - - @override - void didUpdateWidget(ZefyrEditableText oldWidget) { - super.didUpdateWidget(oldWidget); - _updateSubscriptions(oldWidget); - focusOrUnfocusIfNeeded(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final scope = ZefyrScope.of(context); - if (_renderContext != scope.renderContext) { - _renderContext?.removeListener(_handleRenderContextChange); - _renderContext = scope.renderContext; - _renderContext.addListener(_handleRenderContextChange); - } - if (_cursorTimer != scope.cursorTimer) { - _cursorTimer?.stop(); - _cursorTimer = scope.cursorTimer; - _cursorTimer.startOrStop(focusNode, selection); - } - focusOrUnfocusIfNeeded(); - } - - @override - void dispose() { - _cancelSubscriptions(); - super.dispose(); - } - - // - // Overridden members of AutomaticKeepAliveClientMixin - // - - @override - bool get wantKeepAlive => focusNode.hasFocus; - - // - // Private members - // - - final ScrollController _scrollController = ScrollController(); - ZefyrRenderContext _renderContext; - CursorTimer _cursorTimer; - InputConnectionController _input; - bool _didAutoFocus = false; - - List _buildChildren(BuildContext context) { - final result = []; - for (var node in document.root.children) { - result.add(_defaultChildBuilder(context, node)); - } - return result; - } - - Widget _defaultChildBuilder(BuildContext context, Node node) { - if (node is LineNode) { - if (node.hasEmbed) { - return new RawZefyrLine(node: node); - } else if (node.style.contains(NotusAttribute.heading)) { - return new ZefyrHeading(node: node); - } - return new ZefyrParagraph(node: node); - } - - final BlockNode block = node; - final blockStyle = block.style.get(NotusAttribute.block); - if (blockStyle == NotusAttribute.block.code) { - return new ZefyrCode(node: block); - } else if (blockStyle == NotusAttribute.block.bulletList) { - return new ZefyrList(node: block); - } else if (blockStyle == NotusAttribute.block.numberList) { - return new ZefyrList(node: block); - } else if (blockStyle == NotusAttribute.block.quote) { - return new ZefyrQuote(node: block); - } - - throw new UnimplementedError('Block format $blockStyle.'); - } - - void _updateSubscriptions([ZefyrEditableText oldWidget]) { - if (oldWidget == null) { - widget.controller.addListener(_handleLocalValueChange); - focusNode.addListener(_handleFocusChange); - return; - } - - if (widget.controller != oldWidget.controller) { - oldWidget.controller.removeListener(_handleLocalValueChange); - widget.controller.addListener(_handleLocalValueChange); - _input.updateRemoteValue(widget.controller.plainTextEditingValue); - } - if (widget.focusNode != oldWidget.focusNode) { - oldWidget.focusNode.removeListener(_handleFocusChange); - widget.focusNode.addListener(_handleFocusChange); - updateKeepAlive(); - } - } - - void _cancelSubscriptions() { - _renderContext.removeListener(_handleRenderContextChange); - widget.controller.removeListener(_handleLocalValueChange); - focusNode.removeListener(_handleFocusChange); - _input.closeConnection(); - _cursorTimer.stop(); - } - - // Triggered for both text and selection changes. - void _handleLocalValueChange() { - if (widget.enabled && - widget.controller.lastChangeSource == ChangeSource.local) { - // Only request keyboard for user actions. - requestKeyboard(); - } - _input.updateRemoteValue(widget.controller.plainTextEditingValue); - _cursorTimer.startOrStop(focusNode, selection); - setState(() { - // nothing to update internally. - }); - } - - void _handleFocusChange() { - _input.openOrCloseConnection( - focusNode, widget.controller.plainTextEditingValue); - _cursorTimer.startOrStop(focusNode, selection); - updateKeepAlive(); - } - - void _handleRemoteValueChange( - int start, String deleted, String inserted, TextSelection selection) { - widget.controller - .replaceText(start, deleted.length, inserted, selection: selection); - } - - void _handleRenderContextChange() { - setState(() { - // nothing to update internally. - }); - } -} diff --git a/zefyr/lib/src/widgets/editor.dart b/zefyr/lib/src/widgets/editor.dart deleted file mode 100644 index 4a37337e..00000000 --- a/zefyr/lib/src/widgets/editor.dart +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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/widgets.dart'; - -import 'controller.dart'; -import 'editable_text.dart'; -import 'image.dart'; -import 'scaffold.dart'; -import 'scope.dart'; -import 'theme.dart'; -import 'toolbar.dart'; - -/// Widget for editing Zefyr documents. -class ZefyrEditor extends StatefulWidget { - const ZefyrEditor({ - Key key, - @required this.controller, - @required this.focusNode, - this.autofocus: true, - this.enabled: true, - this.padding: const EdgeInsets.symmetric(horizontal: 16.0), - this.toolbarDelegate, - this.imageDelegate, - this.physics, - }) : super(key: key); - - final ZefyrController controller; - final FocusNode focusNode; - final bool autofocus; - final bool enabled; - final ZefyrToolbarDelegate toolbarDelegate; - final ZefyrImageDelegate imageDelegate; - final ScrollPhysics physics; - - /// Padding around editable area. - final EdgeInsets padding; - - @override - _ZefyrEditorState createState() => new _ZefyrEditorState(); -} - -class _ZefyrEditorState extends State { - ZefyrImageDelegate _imageDelegate; - ZefyrScope _scope; - ZefyrThemeData _themeData; - GlobalKey _toolbarKey; - ZefyrScaffoldState _scaffold; - - bool get hasToolbar => _toolbarKey != null; - - void showToolbar() { - assert(_toolbarKey == null); - _toolbarKey = GlobalKey(); - _scaffold.showToolbar(buildToolbar); - } - - void hideToolbar() { - if (_toolbarKey == null) return; - _scaffold.hideToolbar(); - _toolbarKey = null; - } - - Widget buildToolbar(BuildContext context) { - return ZefyrTheme( - data: _themeData, - child: ZefyrToolbar( - key: _toolbarKey, - editor: _scope, - delegate: widget.toolbarDelegate, - ), - ); - } - - void _handleChange() { - if (_scope.focusOwner == FocusOwner.none) { - hideToolbar(); - } else if (!hasToolbar) { - showToolbar(); - } else { - // TODO: is there a nicer way to do this? - WidgetsBinding.instance.addPostFrameCallback((_) { - _toolbarKey?.currentState?.markNeedsRebuild(); - }); - } - } - - @override - void initState() { - super.initState(); - _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); - } - - @override - void didUpdateWidget(ZefyrEditor oldWidget) { - super.didUpdateWidget(oldWidget); - _scope.controller = widget.controller; - _scope.focusNode = widget.focusNode; - if (widget.imageDelegate != oldWidget.imageDelegate) { - _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); - _scope.imageDelegate = _imageDelegate; - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final parentTheme = ZefyrTheme.of(context, nullOk: true); - final fallbackTheme = ZefyrThemeData.fallback(context); - _themeData = (parentTheme != null) - ? fallbackTheme.merge(parentTheme) - : fallbackTheme; - - if (_scope == null) { - _scope = ZefyrScope.editable( - imageDelegate: _imageDelegate, - controller: widget.controller, - focusNode: widget.focusNode, - focusScope: FocusScope.of(context), - ); - _scope.addListener(_handleChange); - } else { - final focusScope = FocusScope.of(context); - _scope.focusScope = focusScope; - } - - final scaffold = ZefyrScaffold.of(context); - if (_scaffold != scaffold) { - bool didHaveToolbar = hasToolbar; - hideToolbar(); - _scaffold = scaffold; - if (didHaveToolbar) showToolbar(); - } - } - - @override - void dispose() { - hideToolbar(); - _scope.removeListener(_handleChange); - _scope.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget editable = new ZefyrEditableText( - controller: _scope.controller, - focusNode: _scope.focusNode, - imageDelegate: _scope.imageDelegate, - autofocus: widget.autofocus, - enabled: widget.enabled, - padding: widget.padding, - physics: widget.physics, - ); - - return ZefyrTheme( - data: _themeData, - child: ZefyrScopeAccess( - scope: _scope, - child: editable, - ), - ); - } -} diff --git a/zefyr/lib/src/widgets/field.dart b/zefyr/lib/src/widgets/field.dart deleted file mode 100644 index d23f3ca7..00000000 --- a/zefyr/lib/src/widgets/field.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'controller.dart'; -import 'editor.dart'; -import 'image.dart'; -import 'toolbar.dart'; - -/// Zefyr editor with material design decorations. -class ZefyrField extends StatefulWidget { - /// Decoration to paint around this editor. - final InputDecoration decoration; - - /// Height of this editor field. - final double height; - final ZefyrController controller; - final FocusNode focusNode; - final bool autofocus; - final bool enabled; - final ZefyrToolbarDelegate toolbarDelegate; - final ZefyrImageDelegate imageDelegate; - final ScrollPhysics physics; - - const ZefyrField({ - Key key, - this.decoration, - this.height, - this.controller, - this.focusNode, - this.autofocus: false, - this.enabled, - this.toolbarDelegate, - this.imageDelegate, - this.physics, - }) : super(key: key); - - @override - _ZefyrFieldState createState() => _ZefyrFieldState(); -} - -class _ZefyrFieldState extends State { - @override - Widget build(BuildContext context) { - Widget child = ZefyrEditor( - padding: EdgeInsets.symmetric(vertical: 6.0), - controller: widget.controller, - focusNode: widget.focusNode, - autofocus: widget.autofocus, - enabled: widget.enabled ?? true, - toolbarDelegate: widget.toolbarDelegate, - imageDelegate: widget.imageDelegate, - physics: widget.physics, - ); - - if (widget.height != null) { - child = ConstrainedBox( - constraints: BoxConstraints.tightFor(height: widget.height), - child: child, - ); - } - - return AnimatedBuilder( - animation: - Listenable.merge([widget.focusNode, widget.controller]), - builder: (BuildContext context, Widget child) { - return InputDecorator( - decoration: _getEffectiveDecoration(), - isFocused: widget.focusNode.hasFocus, - isEmpty: widget.controller.document.length == 1, - child: child, - ); - }, - child: child, - ); - } - - InputDecoration _getEffectiveDecoration() { - final InputDecoration effectiveDecoration = - (widget.decoration ?? const InputDecoration()) - .applyDefaults(Theme.of(context).inputDecorationTheme) - .copyWith( - enabled: widget.enabled ?? true, - ); - - return effectiveDecoration; - } -} diff --git a/zefyr/lib/src/widgets/horizontal_rule.dart b/zefyr/lib/src/widgets/horizontal_rule.dart deleted file mode 100644 index d9011f0e..00000000 --- a/zefyr/lib/src/widgets/horizontal_rule.dart +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:notus/notus.dart'; - -import 'editable_box.dart'; - -class ZefyrHorizontalRule extends LeafRenderObjectWidget { - ZefyrHorizontalRule({@required this.node}) : assert(node != null); - - final EmbedNode node; - - @override - RenderHorizontalRule createRenderObject(BuildContext context) { - return new RenderHorizontalRule(node: node); - } - - @override - void updateRenderObject( - BuildContext context, RenderHorizontalRule renderObject) { - renderObject..node = node; - } -} - -class RenderHorizontalRule extends RenderEditableBox { - static const _kPaddingBottom = 24.0; - static const _kThickness = 3.0; - static const _kHeight = _kThickness + _kPaddingBottom; - - RenderHorizontalRule({ - @required EmbedNode node, - }) : _node = node; - - @override - EmbedNode get node => _node; - EmbedNode _node; - set node(EmbedNode value) { - if (_node == value) return; - _node = value; - markNeedsPaint(); - } - - @override - double get preferredLineHeight => size.height; - - @override - SelectionOrder get selectionOrder => SelectionOrder.background; - - @override - List getEndpointsForSelection(TextSelection selection) { - TextSelection local = getLocalSelection(selection); - if (local.isCollapsed) { - final dx = local.extentOffset == 0 ? 0.0 : size.width; - return [ - new ui.TextBox.fromLTRBD(dx, 0.0, dx, size.height, TextDirection.ltr), - ]; - } - - return [ - new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr), - new ui.TextBox.fromLTRBD( - size.width, 0.0, size.width, size.height, TextDirection.ltr), - ]; - } - - @override - void performLayout() { - assert(constraints.hasBoundedWidth); - size = new Size(constraints.maxWidth, _kHeight); - } - - @override - void paint(PaintingContext context, Offset offset) { - final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kThickness); - final paint = new ui.Paint()..color = Colors.grey.shade200; - context.canvas.drawRect(rect.shift(offset), paint); - } - - @override - TextPosition getPositionForOffset(Offset offset) { - int position = _node.documentOffset; - - if (offset.dx > size.width / 2) { - position++; - } - return new TextPosition(offset: position); - } - - @override - TextRange getWordBoundary(TextPosition position) { - final start = _node.documentOffset; - return new TextRange(start: start, end: start + 1); - } - - @override - void paintSelection(PaintingContext context, Offset offset, - TextSelection selection, Color selectionColor) { - final localSelection = getLocalSelection(selection); - assert(localSelection != null); - if (!localSelection.isCollapsed) { - final Paint paint = new Paint()..color = selectionColor; - final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kHeight); - context.canvas.drawRect(rect.shift(offset), paint); - } - } - - @override - Offset getOffsetForCaret(ui.TextPosition position, ui.Rect caretPrototype) { - final pos = position.offset - node.documentOffset; - Offset caretOffset = Offset.zero; - if (pos == 1) { - caretOffset = caretOffset + new Offset(size.width - 1.0, 0.0); - } - return caretOffset; - } -} diff --git a/zefyr/lib/src/widgets/image.dart b/zefyr/lib/src/widgets/image.dart deleted file mode 100644 index be096d06..00000000 --- a/zefyr/lib/src/widgets/image.dart +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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:io'; -import 'dart:math' as math; -import 'dart:ui' as ui; - -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:notus/notus.dart'; -import 'package:image_picker/image_picker.dart'; - -import 'editable_box.dart'; - -abstract class ZefyrImageDelegate { - /// Builds image widget for specified [imageSource] and [context]. - Widget buildImage(BuildContext context, String imageSource); - - /// Picks an image from specified [source]. - /// - /// Returns unique string key for the selected image. Returned key is stored - /// in the document. - Future pickImage(S source); -} - -class ZefyrDefaultImageDelegate implements ZefyrImageDelegate { - @override - Widget buildImage(BuildContext context, String imageSource) { - final file = new File.fromUri(Uri.parse(imageSource)); - final image = new FileImage(file); - return new Image(image: image); - } - - @override - Future pickImage(ImageSource source) async { - final file = await ImagePicker.pickImage(source: source); - if (file == null) return null; - return file.uri.toString(); - } -} - -class ZefyrImage extends StatefulWidget { - const ZefyrImage({Key key, @required this.node, @required this.delegate}) - : super(key: key); - - final EmbedNode node; - final ZefyrImageDelegate delegate; - - @override - _ZefyrImageState createState() => _ZefyrImageState(); -} - -class _ZefyrImageState extends State { - String get imageSource { - EmbedAttribute attribute = widget.node.style.get(NotusAttribute.embed); - return attribute.value['source'] as String; - } - - @override - Widget build(BuildContext context) { - final image = widget.delegate.buildImage(context, imageSource); - return _EditableImage( - child: image, - node: widget.node, - ); - } -} - -class _EditableImage extends SingleChildRenderObjectWidget { - _EditableImage({@required Widget child, @required this.node}) - : assert(node != null), - super(child: child); - - final EmbedNode node; - - @override - RenderEditableImage createRenderObject(BuildContext context) { - return new RenderEditableImage(node: node); - } - - @override - void updateRenderObject( - BuildContext context, RenderEditableImage renderObject) { - renderObject..node = node; - } -} - -class RenderEditableImage extends RenderBox - with RenderObjectWithChildMixin, RenderProxyBoxMixin - implements RenderEditableBox { - static const kPaddingBottom = 24.0; - - RenderEditableImage({ - RenderImage child, - @required EmbedNode node, - }) : _node = node { - this.child = child; - } - - @override - EmbedNode get node => _node; - EmbedNode _node; - void set node(EmbedNode value) { - _node = value; - } - - // TODO: Customize caret height offset instead of adjusting here by 2px. - @override - double get preferredLineHeight => size.height - kPaddingBottom + 2.0; - - @override - SelectionOrder get selectionOrder => SelectionOrder.foreground; - - @override - TextSelection getLocalSelection(TextSelection documentSelection) { - if (!intersectsWithSelection(documentSelection)) return null; - - int nodeBase = node.documentOffset; - int nodeExtent = nodeBase + node.length; - int base = math.max(0, documentSelection.baseOffset - nodeBase); - int extent = - math.min(documentSelection.extentOffset, nodeExtent) - nodeBase; - return documentSelection.copyWith(baseOffset: base, extentOffset: extent); - } - - @override - List getEndpointsForSelection(TextSelection selection) { - TextSelection local = getLocalSelection(selection); - if (local.isCollapsed) { - final dx = local.extentOffset == 0 ? _childOffset.dx : size.width; - return [ - new ui.TextBox.fromLTRBD( - dx, 0.0, dx, size.height - kPaddingBottom, TextDirection.ltr), - ]; - } - - final rect = _childRect; - return [ - new ui.TextBox.fromLTRBD( - rect.left, rect.top, rect.left, rect.bottom, TextDirection.ltr), - new ui.TextBox.fromLTRBD( - rect.right, rect.top, rect.right, rect.bottom, TextDirection.ltr), - ]; - } - - @override - TextPosition getPositionForOffset(Offset offset) { - int position = _node.documentOffset; - - if (offset.dx > size.width / 2) { - position++; - } - return new TextPosition(offset: position); - } - - @override - TextRange getWordBoundary(TextPosition position) { - final start = _node.documentOffset; - return new TextRange(start: start, end: start + 1); - } - - @override - bool intersectsWithSelection(TextSelection selection) { - final int base = node.documentOffset; - final int extent = base + node.length; - return base <= selection.extentOffset && selection.baseOffset <= extent; - } - - @override - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { - final pos = position.offset - node.documentOffset; - Offset caretOffset = _childOffset - new Offset(kHorizontalPadding, 0.0); - if (pos == 1) { - caretOffset = caretOffset + - new Offset(_lastChildSize.width + kHorizontalPadding, 0.0); - } - return caretOffset; - } - - @override - void paintSelection(PaintingContext context, Offset offset, - TextSelection selection, Color selectionColor) { - final localSelection = getLocalSelection(selection); - assert(localSelection != null); - if (!localSelection.isCollapsed) { - final Paint paint = new Paint() - ..color = selectionColor - ..style = PaintingStyle.stroke - ..strokeWidth = 3.0; - final rect = new Rect.fromLTWH( - 0.0, 0.0, _lastChildSize.width, _lastChildSize.height); - context.canvas.drawRect(rect.shift(offset + _childOffset), paint); - } - } - - void paint(PaintingContext context, Offset offset) { - super.paint(context, offset + _childOffset); - } - - static const double kHorizontalPadding = 1.0; - - Size _lastChildSize; - - Offset get _childOffset { - final dx = (size.width - _lastChildSize.width) / 2 + kHorizontalPadding; - final dy = (size.height - _lastChildSize.height - kPaddingBottom) / 2; - return new Offset(dx, dy); - } - - Rect get _childRect { - return new Rect.fromLTWH(_childOffset.dx, _childOffset.dy, - _lastChildSize.width, _lastChildSize.height); - } - - @override - void performLayout() { - assert(constraints.hasBoundedWidth); - if (child != null) { - // Make constraints use 16:9 aspect ratio. - final width = constraints.maxWidth - kHorizontalPadding * 2; - final childConstraints = constraints.copyWith( - minWidth: 0.0, - maxWidth: width, - minHeight: 0.0, - maxHeight: (width * 9 / 16).floorToDouble(), - ); - child.layout(childConstraints, parentUsesSize: true); - _lastChildSize = child.size; - size = new Size( - constraints.maxWidth, _lastChildSize.height + kPaddingBottom); - } else { - performResize(); - } - } -} diff --git a/zefyr/lib/src/widgets/input.dart b/zefyr/lib/src/widgets/input.dart deleted file mode 100644 index d006d976..00000000 --- a/zefyr/lib/src/widgets/input.dart +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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/services.dart'; -import 'package:flutter/src/foundation/diagnostics.dart'; -import 'package:flutter/widgets.dart'; -import 'package:zefyr/util.dart'; - -typedef RemoteValueChanged = Function( - int start, String deleted, String inserted, TextSelection selection); - -class InputConnectionController implements TextInputClient { - InputConnectionController(this.onValueChanged) - : assert(onValueChanged != null); - - // - // New public members - // - - final RemoteValueChanged onValueChanged; - - /// Returns `true` if there is open input connection. - bool get hasConnection => - _textInputConnection != null && _textInputConnection.attached; - - /// Opens or closes input connection based on the current state of - /// [focusNode] and [value]. - void openOrCloseConnection(FocusNode focusNode, TextEditingValue value) { - if (focusNode.hasFocus && focusNode.consumeKeyboardToken()) { - openConnection(value); - } else if (!focusNode.hasFocus) { - closeConnection(); - } - } - - void openConnection(TextEditingValue value) { - if (!hasConnection) { - _lastKnownRemoteTextEditingValue = value; - _textInputConnection = TextInput.attach( - this, - new TextInputConfiguration( - inputType: TextInputType.multiline, - obscureText: false, - autocorrect: true, - inputAction: TextInputAction.newline, - textCapitalization: TextCapitalization.sentences, - ), - )..setEditingState(value); - _sentRemoteValues.add(value); - } - _textInputConnection.show(); - } - - /// Closes input connection if it's currently open. Otherwise does nothing. - void closeConnection() { - if (hasConnection) { - _textInputConnection.close(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } - } - - /// Updates remote value based on current state of [document] and - /// [selection]. - /// - /// This method may not actually send an update to native side if it thinks - /// remote value is up to date or identical. - void updateRemoteValue(TextEditingValue value) { - if (!hasConnection) return; - - // Since we don't keep track of composing range in value provided by - // ZefyrController we need to add it here manually before comparing - // with the last known remote value. - // It is important to prevent excessive remote updates as it can cause - // race conditions. - final actualValue = value.copyWith( - composing: _lastKnownRemoteTextEditingValue.composing, - ); - - if (actualValue == _lastKnownRemoteTextEditingValue) return; - - bool shouldRemember = value.text != _lastKnownRemoteTextEditingValue.text; - _lastKnownRemoteTextEditingValue = actualValue; - _textInputConnection.setEditingState(actualValue); - if (shouldRemember) { - // Only keep track if text changed (selection changes are not relevant) - _sentRemoteValues.add(actualValue); - } - } - - // - // Overridden members - // - - @override - void performAction(TextInputAction action) { - // no-op - } - - @override - void updateEditingValue(TextEditingValue value) { - if (_sentRemoteValues.contains(value)) { - /// There is a race condition in Flutter text input plugin where sending - /// updates to native side too often results in broken behavior. - /// TextInputConnection.setEditingValue is an async call to native side. - /// For each such call native side _always_ sends update which triggers - /// this method (updateEditingValue) with the same value we've sent it. - /// If multiple calls to setEditingValue happen too fast and we only - /// track the last sent value then there is no way for us to filter out - /// automatic callbacks from native side. - /// Therefore we have to keep track of all values we send to the native - /// side and when we see this same value appear here we skip it. - /// This is fragile but it's probably the only available option. - _sentRemoteValues.remove(value); - return; - } - - if (_lastKnownRemoteTextEditingValue == value) { - // There is no difference between this value and the last known value. - return; - } - - // Check if only composing range changed. - if (_lastKnownRemoteTextEditingValue.text == value.text && - _lastKnownRemoteTextEditingValue.selection == value.selection) { - // This update only modifies composing range. Since we don't keep track - // of composing range in Zefyr we just need to update last known value - // here. - // Note: this check fixes an issue on Android when it sends - // composing updates separately from regular changes for text and - // selection. - _lastKnownRemoteTextEditingValue = value; - return; - } - - // Note Flutter (unintentionally?) silences errors occurred during - // text input update, so we have to report it ourselves. - // For more details see https://github.com/flutter/flutter/issues/19191 - // TODO: remove try-catch when/if Flutter stops silencing these errors. - try { - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue; - _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = fastDiff(oldText, text, cursorPosition); - onValueChanged(diff.start, diff.deleted, diff.inserted, value.selection); - } catch (e, trace) { - FlutterError.reportError(new FlutterErrorDetails( - exception: e, - stack: trace, - library: 'Zefyr', -// context: 'while updating editing value', - context: new TextNode() - )); - rethrow; - } - } - - - // - // Private members - // - - final List _sentRemoteValues = []; - TextInputConnection _textInputConnection; - TextEditingValue _lastKnownRemoteTextEditingValue; - - @override - void updateFloatingCursor(RawFloatingCursorPoint point) { - // TODO: implement updateFloatingCursor - } -} - -class TextNode extends DiagnosticsNode{ - @override - List getChildren() { - // TODO: implement getChildren - return null; - } - - @override - List getProperties() { - // TODO: implement getProperties - return null; - } - - @override - String toDescription({TextTreeConfiguration parentConfiguration}) { - // TODO: implement toDescription - return null; - } - - @override - Object get value => 'while updating editing value'; - -} diff --git a/zefyr/lib/src/widgets/list.dart b/zefyr/lib/src/widgets/list.dart deleted file mode 100644 index a3479e58..00000000 --- a/zefyr/lib/src/widgets/list.dart +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; -import 'package:notus/notus.dart'; - -import 'common.dart'; -import 'paragraph.dart'; -import 'theme.dart'; - -/// Represents number lists and bullet lists in a Zefyr editor. -class ZefyrList extends StatelessWidget { - const ZefyrList({Key key, @required this.node}) : super(key: key); - - final BlockNode node; - - @override - Widget build(BuildContext context) { - final theme = ZefyrTheme.of(context); - List items = []; - int index = 1; - for (var line in node.children) { - items.add(_buildItem(line, index)); - index++; - } - - final isNumberList = - node.style.get(NotusAttribute.block) == NotusAttribute.block.numberList; - EdgeInsets padding = isNumberList - ? theme.blockTheme.numberList.padding - : theme.blockTheme.bulletList.padding; - padding = padding.copyWith(left: theme.indentSize); - - return new Padding( - padding: padding, - child: new Column(children: items), - ); - } - - Widget _buildItem(Node node, int index) { - LineNode line = node; - return new ZefyrListItem(index: index, node: line); - } -} - -/// An item in a [ZefyrList]. -class ZefyrListItem extends StatelessWidget { - ZefyrListItem({Key key, this.index, this.node}) : super(key: key); - - final int index; - final LineNode node; - - @override - Widget build(BuildContext context) { - final BlockNode block = node.parent; - final style = block.style.get(NotusAttribute.block); - final theme = ZefyrTheme.of(context); - final bulletText = - (style == NotusAttribute.block.bulletList) ? '•' : '$index.'; - - TextStyle textStyle; - Widget content; - EdgeInsets padding; - - if (node.style.contains(NotusAttribute.heading)) { - final headingTheme = ZefyrHeading.themeOf(node, context); - textStyle = headingTheme.textStyle; - padding = headingTheme.padding; - content = new ZefyrHeading(node: node); - } else { - textStyle = theme.paragraphTheme.textStyle; - content = new RawZefyrLine(node: node, style: textStyle); - } - - Widget bullet = - SizedBox(width: 24.0, child: Text(bulletText, style: textStyle)); - if (padding != null) { - bullet = Padding(padding: padding, child: bullet); - } - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [bullet, Expanded(child: content)], - ); - } -} diff --git a/zefyr/lib/src/widgets/paragraph.dart b/zefyr/lib/src/widgets/paragraph.dart deleted file mode 100644 index 222b5bae..00000000 --- a/zefyr/lib/src/widgets/paragraph.dart +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; -import 'package:notus/notus.dart'; - -import 'common.dart'; -import 'theme.dart'; - -/// Represents regular paragraph line in a Zefyr editor. -class ZefyrParagraph extends StatelessWidget { - ZefyrParagraph({Key key, @required this.node, this.blockStyle}) - : super(key: key); - - final LineNode node; - final TextStyle blockStyle; - - @override - Widget build(BuildContext context) { - final theme = ZefyrTheme.of(context); - TextStyle style = theme.paragraphTheme.textStyle; - if (blockStyle != null) { - style = style.merge(blockStyle); - } - return new RawZefyrLine( - node: node, - style: style, - padding: theme.paragraphTheme.padding, - ); - } -} - -/// Represents heading-styled line in [ZefyrEditor]. -class ZefyrHeading extends StatelessWidget { - ZefyrHeading({Key key, @required this.node, this.blockStyle}) - : assert(node.style.contains(NotusAttribute.heading)), - super(key: key); - - final LineNode node; - final TextStyle blockStyle; - - @override - Widget build(BuildContext context) { - final theme = themeOf(node, context); - TextStyle style = theme.textStyle; - if (blockStyle != null) { - style = style.merge(blockStyle); - } - return new RawZefyrLine( - node: node, - style: style, - padding: theme.padding, - ); - } - - static StyleTheme themeOf(LineNode node, BuildContext context) { - final theme = ZefyrTheme.of(context); - final style = node.style.get(NotusAttribute.heading); - if (style == NotusAttribute.heading.level1) { - return theme.headingTheme.level1; - } else if (style == NotusAttribute.heading.level2) { - return theme.headingTheme.level2; - } else if (style == NotusAttribute.heading.level3) { - return theme.headingTheme.level3; - } - throw new UnimplementedError('Unsupported heading style $style'); - } -} diff --git a/zefyr/lib/src/widgets/quote.dart b/zefyr/lib/src/widgets/quote.dart deleted file mode 100644 index a9eacd13..00000000 --- a/zefyr/lib/src/widgets/quote.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; -import 'package:notus/notus.dart'; - -import 'paragraph.dart'; -import 'theme.dart'; - -/// Represents a quote block in a Zefyr editor. -class ZefyrQuote extends StatelessWidget { - const ZefyrQuote({Key key, @required this.node}) : super(key: key); - - final BlockNode node; - - @override - Widget build(BuildContext context) { - final theme = ZefyrTheme.of(context); - final style = theme.blockTheme.quote.textStyle; - List items = []; - for (var line in node.children) { - items.add(_buildLine(line, style, theme.indentSize)); - } - - return Padding( - padding: theme.blockTheme.quote.padding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: items, - ), - ); - } - - Widget _buildLine(Node node, TextStyle blockStyle, double indentSize) { - LineNode line = node; - - Widget content; - if (line.style.contains(NotusAttribute.heading)) { - content = new ZefyrHeading(node: line, blockStyle: blockStyle); - } else { - content = new ZefyrParagraph(node: line, blockStyle: blockStyle); - } - - final row = Row(children: [Expanded(child: content)]); - return Container( - decoration: BoxDecoration( - border: Border( - left: BorderSide(width: 4.0, color: Colors.grey.shade300), - ), - ), - padding: EdgeInsets.only(left: indentSize), - child: row, - ); - } -} diff --git a/zefyr/lib/src/widgets/render_context.dart b/zefyr/lib/src/widgets/render_context.dart deleted file mode 100644 index 1e88aa53..00000000 --- a/zefyr/lib/src/widgets/render_context.dart +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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/widgets.dart'; - -import 'editable_box.dart'; - -/// Registry of all [RenderEditableProxyBox]es inside a [ZefyrEditableText]. -/// -/// Provides access to all currently active [RenderEditableProxyBox] -/// instances of a [ZefyrEditableText]. -/// -/// Use [boxForTextOffset] or [boxForGlobalPoint] to retrieve a -/// specific box. -/// -/// The [addBox], [removeBox] and [markDirty] are intended to be -/// only used by [RenderEditableProxyBox] objects to register with a rendering -/// context. -/// -/// ### Life cycle details -/// -/// When a box object is attached to rendering pipeline it registers -/// itself with a render scope by calling [addBox]. At this point the context -/// treats this object as "dirty" and query methods like [boxForTextOffset] -/// still exclude this object from returned results. -/// -/// When this box considers itself initialized it calls [markDirty] with -/// `isDirty` set to `false` which activates it. At this point query methods -/// include this object in results. -/// -/// When a box is rebuilt it may deactivate itself by calling [markDirty] -/// again. -/// -/// When a box is detached from rendering pipeline it unregisters -/// itself by calling [removeBox]. -class ZefyrRenderContext extends ChangeNotifier { - final Set _dirtyBoxes = new Set(); - final Set _activeBoxes = new Set(); - - Set get dirty => _dirtyBoxes; - Set get active => _activeBoxes; - - bool _disposed = false; - - /// Adds [box] to this context. The box is considered "dirty" at - /// this point and is not included in query results of `boxFor*` - /// methods. - void addBox(RenderEditableProxyBox box) { - assert(!_disposed); - _dirtyBoxes.add(box); - } - - /// Removes [box] from this render context. - void removeBox(RenderEditableProxyBox box) { - assert(!_disposed); - _dirtyBoxes.remove(box); - _activeBoxes.remove(box); - notifyListeners(); - } - - void markDirty(RenderEditableProxyBox box, bool isDirty) { - assert(!_disposed); - - var collection = isDirty ? _dirtyBoxes : _activeBoxes; - if (collection.contains(box)) return; - - if (isDirty) { - _activeBoxes.remove(box); - _dirtyBoxes.add(box); - } else { - _dirtyBoxes.remove(box); - _activeBoxes.add(box); - } - notifyListeners(); - } - - /// Returns box containing character at specified document [offset]. - RenderEditableProxyBox boxForTextOffset(int offset) { - assert(!_disposed); - return _activeBoxes.firstWhere( - (p) => p.node.containsOffset(offset), - orElse: _null, - ); - } - - /// Returns box located at specified global [point] on the screen or - /// `null`. - RenderEditableProxyBox boxForGlobalPoint(Offset point) { - assert(!_disposed); - return _activeBoxes.firstWhere((p) { - final localPoint = p.globalToLocal(point); - return p.size.contains(localPoint); - }, orElse: _null); - } - - /// Returns closest render box to the specified global [point]. - /// - /// If [point] is inside of one of active render boxes that box is returned. - /// If no box found this method checks if [point] is to the left or to the right - /// side of a box, e.g. if vertical offset of this point is inside of one of - /// the active boxes. If it is then that box is returned. If not then this - /// method picks a box with shortest vertical distance to this [point]. - RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) { - assert(!_disposed); - if (_activeBoxes.isEmpty) return null; - RenderEditableProxyBox box = boxForGlobalPoint(point); - if (box != null) return box; - - box = _activeBoxes.firstWhere((p) { - final localPoint = p.globalToLocal(point); - return (localPoint.dy >= 0 && localPoint.dy < p.size.height); - }, orElse: _null); - if (box != null) return box; - - box = _activeBoxes.map((p) { - final localPoint = p.globalToLocal(point); - final distance = localPoint.dy - p.size.height; - return new MapEntry(distance.abs(), p); - }).reduce((a, b) { - return (a.key <= b.key) ? a : b; - }).value; - - return box; - } - - static Null _null() => null; - - @override - void dispose() { - _disposed = true; - _activeBoxes.clear(); - _dirtyBoxes.clear(); - super.dispose(); - } - - @override - void notifyListeners() { - /// Ensures listeners are not notified during rendering phase where they - /// cannot react by updating their state or rebuilding. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_disposed) return; - super.notifyListeners(); - }); - } -} diff --git a/zefyr/lib/src/widgets/rich_text.dart b/zefyr/lib/src/widgets/rich_text.dart deleted file mode 100644 index f2eff9c9..00000000 --- a/zefyr/lib/src/widgets/rich_text.dart +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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' as ui; - -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:notus/notus.dart'; - -import 'caret.dart'; -import 'editable_box.dart'; - -/// Represents single paragraph of Zefyr rich-text. -class ZefyrRichText extends LeafRenderObjectWidget { - ZefyrRichText({ - @required this.node, - @required this.text, - }) : assert(node != null && text != null); - - final LineNode node; - final TextSpan text; - - @override - RenderObject createRenderObject(BuildContext context) { - return new RenderZefyrParagraph( - text, - node: node, - textDirection: Directionality.of(context), - ); - } - - @override - void updateRenderObject( - BuildContext context, RenderZefyrParagraph renderObject) { - renderObject - ..text = text - ..node = node; - } -} - -class RenderZefyrParagraph extends RenderParagraph - implements RenderEditableBox { - RenderZefyrParagraph( - TextSpan text, { - @required LineNode node, - TextAlign textAlign: TextAlign.start, - @required TextDirection textDirection, - bool softWrap: true, - TextOverflow overflow: TextOverflow.clip, - double textScaleFactor: 1.0, - int maxLines, - }) : _node = node, - _prototypePainter = new TextPainter( - text: new TextSpan(text: '.', style: text.style), - textAlign: textAlign, - textDirection: textDirection, - textScaleFactor: textScaleFactor, - ), - super( - text, - textAlign: textAlign, - textDirection: textDirection, - softWrap: softWrap, - overflow: overflow, - textScaleFactor: textScaleFactor, - maxLines: maxLines, - ); - - LineNode get node => _node; - LineNode _node; - void set node(LineNode value) { - _node = value; - } - - @override - double get preferredLineHeight => _prototypePainter.height; - - @override - SelectionOrder get selectionOrder => SelectionOrder.background; - - @override - TextSelection getLocalSelection(TextSelection documentSelection) { - if (!intersectsWithSelection(documentSelection)) return null; - - int nodeBase = node.documentOffset; - int nodeExtent = nodeBase + node.length; - int base = math.max(0, documentSelection.baseOffset - nodeBase); - int extent = - math.min(documentSelection.extentOffset, nodeExtent) - nodeBase; - return documentSelection.copyWith(baseOffset: base, extentOffset: extent); - } - - @override - TextPosition getPositionForOffset(Offset offset) { - final position = super.getPositionForOffset(offset); - return new TextPosition( - offset: _node.documentOffset + position.offset, - affinity: position.affinity, - ); - } - - @override - TextRange getWordBoundary(TextPosition position) { - final localPosition = new TextPosition( - offset: position.offset - _node.documentOffset, - affinity: position.affinity, - ); - final localRange = super.getWordBoundary(localPosition); - return new TextRange( - start: _node.documentOffset + localRange.start, - end: _node.documentOffset + localRange.end, - ); - } - - @override - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { - final localPosition = new TextPosition( - offset: position.offset - _node.documentOffset, - affinity: position.affinity, - ); - return super.getOffsetForCaret(localPosition, caretPrototype); - } - - // This method works around some issues in getBoxesForSelection and handles - // edge-case with our TextSpan objects not having last line-break character. - @override - List getEndpointsForSelection(TextSelection selection) { - TextSelection local = getLocalSelection(selection); - if (local.isCollapsed) { - final caret = CursorPainter.buildPrototype(preferredLineHeight); - final offset = getOffsetForCaret(local.extent, caret); - return [ - new ui.TextBox.fromLTRBD( - offset.dx, - offset.dy, - offset.dx, - offset.dy + caret.height, - TextDirection.ltr, - ) - ]; - } - - int isBaseShifted = 0; - bool isExtentShifted = false; - if (local.baseOffset == node.length - 1 && local.baseOffset > 0) { - // Since we exclude last line-break from rendered TextSpan we have to - // handle end-of-line selection explicitly. - local = local.copyWith(baseOffset: local.baseOffset - 1); - isBaseShifted = -1; - } else if (local.baseOffset == 0 && local.isCollapsed) { - // This takes care of beginning of line position. - local = local.copyWith(baseOffset: local.baseOffset + 1); - isBaseShifted = 1; - } - if (text.codeUnitAt(local.extentOffset - 1) == 0xA) { - // This takes care of the rest end-of-line scenarios, where there are - // actually line-breaks in the TextSpan (e.g. in code blocks). - local = local.copyWith(extentOffset: local.extentOffset + 1); - isExtentShifted = true; - } - final result = getBoxesForSelection(local).toList(); - if (isBaseShifted != 0) { - final box = result.first; - final dx = isBaseShifted == -1 ? box.right : box.left; - result.removeAt(0); - result.insert(0, - new ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction)); - } - if (isExtentShifted) { - final box = result.last; - result.removeLast; - result.add(new ui.TextBox.fromLTRBD( - box.left, box.top, box.left, box.bottom, box.direction)); - } - return result; - } - - @override - void set text(InlineSpan value) { - _prototypePainter.text = new TextSpan(text: '.', style: value.style); - _selectionRects = null; - super.text = value; - } - - @override - void performLayout() { - super.performLayout(); - _prototypePainter.layout( - minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); - } - - @override - void paint(PaintingContext context, Offset offset) { - super.paint(context, offset); - } - - final TextPainter _prototypePainter; - List _selectionRects; - - /// Returns `true` if this paragraph intersects with document [selection]. - @override - bool intersectsWithSelection(TextSelection selection) { - final int base = node.documentOffset; - final int extent = base + node.length; - return base <= selection.extentOffset && selection.baseOffset <= extent; - } - - TextSelection _lastPaintedSelection; - @override - void paintSelection(PaintingContext context, Offset offset, - TextSelection selection, Color selectionColor) { - if (_lastPaintedSelection != selection) { - _selectionRects = null; - } - _selectionRects ??= getBoxesForSelection(getLocalSelection(selection)); - final Paint paint = new Paint()..color = selectionColor; - for (ui.TextBox box in _selectionRects) { - context.canvas.drawRect(box.toRect().shift(offset), paint); - } - _lastPaintedSelection = selection; - } -} diff --git a/zefyr/lib/src/widgets/scaffold.dart b/zefyr/lib/src/widgets/scaffold.dart deleted file mode 100644 index f711070e..00000000 --- a/zefyr/lib/src/widgets/scaffold.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -class ZefyrScaffold extends StatefulWidget { - final Widget child; - - const ZefyrScaffold({Key key, this.child}) : super(key: key); - - static ZefyrScaffoldState of(BuildContext context) { - final _ZefyrScaffoldAccess widget = - context.inheritFromWidgetOfExactType(_ZefyrScaffoldAccess); - return widget.scaffold; - } - - @override - ZefyrScaffoldState createState() => ZefyrScaffoldState(); -} - -class ZefyrScaffoldState extends State { - WidgetBuilder _toolbarBuilder; - - void showToolbar(WidgetBuilder builder) { - setState(() { - _toolbarBuilder = builder; - }); - } - - void hideToolbar() { - if (_toolbarBuilder != null) { - setState(() { - _toolbarBuilder = null; - }); - } - } - - @override - Widget build(BuildContext context) { - final toolbar = - (_toolbarBuilder == null) ? Container() : _toolbarBuilder(context); - return _ZefyrScaffoldAccess( - scaffold: this, - child: Column( - children: [ - Expanded(child: widget.child), - toolbar, - ], - ), - ); - } -} - -class _ZefyrScaffoldAccess extends InheritedWidget { - final ZefyrScaffoldState scaffold; - - _ZefyrScaffoldAccess({Widget child, this.scaffold}) : super(child: child); - - @override - bool updateShouldNotify(_ZefyrScaffoldAccess oldWidget) { - return oldWidget.scaffold != scaffold; - } -} diff --git a/zefyr/lib/src/widgets/scope.dart b/zefyr/lib/src/widgets/scope.dart deleted file mode 100644 index 2853a8a2..00000000 --- a/zefyr/lib/src/widgets/scope.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:notus/notus.dart'; - -import 'controller.dart'; -import 'cursor_timer.dart'; -import 'editor.dart'; -import 'image.dart'; -import 'render_context.dart'; -import 'view.dart'; - -/// Provides access to shared state of [ZefyrEditor] or [ZefyrView]. -/// -/// A scope object can be created by an editable widget like [ZefyrEditor] in -/// which case it provides access to editing state, including focus nodes, -/// selection and such. Editable scope can be created using -/// [ZefyrScope.editable] constructor. -/// -/// If a scope object is created by a view-only widget like [ZefyrView] then -/// it only provides access to [imageDelegate]. -/// -/// Can be retrieved using [ZefyrScope.of]. -class ZefyrScope extends ChangeNotifier { - /// Creates a view-only scope. - /// - /// Normally used in [ZefyrView]. - ZefyrScope.view({@required ZefyrImageDelegate imageDelegate}) - : assert(imageDelegate != null), - isEditable = false, - _imageDelegate = imageDelegate; - - /// Creates editable scope. - /// - /// Normally used in [ZefyrEditor]. - ZefyrScope.editable({ - @required ZefyrController controller, - @required ZefyrImageDelegate imageDelegate, - @required FocusNode focusNode, - @required FocusScopeNode focusScope, - }) : assert(controller != null), - assert(imageDelegate != null), - assert(focusNode != null), - assert(focusScope != null), - isEditable = true, - _controller = controller, - _imageDelegate = imageDelegate, - _focusNode = focusNode, - _focusScope = focusScope, - _cursorTimer = CursorTimer(), - _renderContext = ZefyrRenderContext() { - _selectionStyle = _controller.getSelectionStyle(); - _selection = _controller.selection; - _controller.addListener(_handleControllerChange); - _focusNode.addListener(_handleFocusChange); - } - - static ZefyrScope of(BuildContext context) { - final ZefyrScopeAccess widget = - context.inheritFromWidgetOfExactType(ZefyrScopeAccess); - return widget.scope; - } - - ZefyrImageDelegate _imageDelegate; - ZefyrImageDelegate get imageDelegate => _imageDelegate; - set imageDelegate(ZefyrImageDelegate value) { - assert(value != null); - if (_imageDelegate != value) { - _imageDelegate = value; - notifyListeners(); - } - } - - ZefyrController _controller; - ZefyrController get controller => _controller; - set controller(ZefyrController value) { - assert(isEditable && value != null); - if (_controller != value) { - _controller.removeListener(_handleControllerChange); - _controller = value; - _selectionStyle = _controller.getSelectionStyle(); - _selection = _controller.selection; - _controller.addListener(_handleControllerChange); - notifyListeners(); - } - } - - FocusNode _focusNode; - FocusNode get focusNode => _focusNode; - set focusNode(FocusNode value) { - assert(isEditable && value != null); - if (_focusNode != value) { - _focusNode.removeListener(_handleFocusChange); - _focusNode = value; - _focusNode.addListener(_handleFocusChange); - notifyListeners(); - } - } - - FocusScopeNode _focusScope; - FocusScopeNode get focusScope => _focusScope; - set focusScope(FocusScopeNode value) { - assert(isEditable && value != null); - if (_focusScope != value) { - _focusScope = value; - } - } - - CursorTimer _cursorTimer; - CursorTimer get cursorTimer => _cursorTimer; - ValueNotifier get showCursor => cursorTimer.value; - - ZefyrRenderContext _renderContext; - ZefyrRenderContext get renderContext => _renderContext; - - NotusStyle get selectionStyle => _selectionStyle; - NotusStyle _selectionStyle; - TextSelection get selection => _selection; - TextSelection _selection; - - bool _disposed = false; - FocusNode _toolbarFocusNode; - - /// Whether this scope is backed by editable Zefyr widgets or read-only view. - /// - /// Returns `true` if this scope provides Zefyr interface that allows editing - /// (e.g. created by [ZefyrEditor]). Returns `false` if this scope provides - /// read-only view (e.g. created by [ZefyrView]). - /// - /// Editable scope provides access to corresponding [controller], [focusNode], - /// [focusScope], [showCursor], [renderContext] and other shared objects. For - /// non-editable scopes these are set to `null`. You can still access - /// objects which are not dependent on editing flow, e.g. [imageDelegate]. - final bool isEditable; - - set toolbarFocusNode(FocusNode node) { - assert(isEditable); - assert(!_disposed || node == null); - if (_toolbarFocusNode != node) { - _toolbarFocusNode?.removeListener(_handleFocusChange); - _toolbarFocusNode = node; - _toolbarFocusNode?.addListener(_handleFocusChange); - // We do not notify listeners here because it will happen when - // focus state changes, see [_handleFocusChange]. - } - } - - FocusOwner get focusOwner { - assert(isEditable); - assert(!_disposed); - if (_focusNode.hasFocus) { - return FocusOwner.editor; - } else if (_toolbarFocusNode?.hasFocus == true) { - return FocusOwner.toolbar; - } else { - return FocusOwner.none; - } - } - - void updateSelection(TextSelection value, - {ChangeSource source: ChangeSource.remote}) { - assert(isEditable); - assert(!_disposed); - _controller.updateSelection(value, source: source); - } - - void formatSelection(NotusAttribute value) { - assert(isEditable); - assert(!_disposed); - _controller.formatSelection(value); - } - - void focus() { - assert(isEditable); - assert(!_disposed); - _focusScope.requestFocus(_focusNode); - } - - void hideKeyboard() { - assert(isEditable); - assert(!_disposed); - _focusNode.unfocus(); - } - - @override - void dispose() { - assert(!_disposed); - _controller?.removeListener(_handleControllerChange); - _focusNode?.removeListener(_handleFocusChange); - _disposed = true; - super.dispose(); - } - - void _handleControllerChange() { - assert(!_disposed); - final attrs = _controller.getSelectionStyle(); - final selection = _controller.selection; - if (_selectionStyle != attrs || _selection != selection) { - _selectionStyle = attrs; - _selection = selection; - notifyListeners(); - } - } - - void _handleFocusChange() { - assert(!_disposed); - if (focusOwner == FocusOwner.none && !_selection.isCollapsed) { - // Collapse selection if there is nothing focused. - _controller.updateSelection(_selection.copyWith( - baseOffset: _selection.extentOffset, - extentOffset: _selection.extentOffset, - )); - } - notifyListeners(); - } - - @override - String toString() { - return '$ZefyrScope#${shortHash(this)}'; - } -} - -class ZefyrScopeAccess extends InheritedWidget { - final ZefyrScope scope; - - ZefyrScopeAccess({Key key, @required this.scope, @required Widget child}) - : super(key: key, child: child); - - @override - bool updateShouldNotify(ZefyrScopeAccess oldWidget) { - return scope != oldWidget.scope; - } -} diff --git a/zefyr/lib/src/widgets/selection.dart b/zefyr/lib/src/widgets/selection.dart deleted file mode 100644 index febbd661..00000000 --- a/zefyr/lib/src/widgets/selection.dart +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:notus/notus.dart'; -import 'package:zefyr/util.dart'; - -import 'controller.dart'; -import 'editable_box.dart'; -import 'scope.dart'; - -RenderEditableBox _getEditableBox(HitTestResult result) { - for (var entry in result.path) { - if (entry.target is RenderEditableBox) { - return entry.target as RenderEditableBox; - } - } - return null; -} - -/// Selection overlay controls selection handles and other gestures. -class ZefyrSelectionOverlay extends StatefulWidget { - const ZefyrSelectionOverlay({ - Key key, - @required this.controller, - @required this.controls, - @required this.overlay, - }) : super(key: key); - - final ZefyrController controller; - final TextSelectionControls controls; - final OverlayState overlay; - - @override - _ZefyrSelectionOverlayState createState() => - new _ZefyrSelectionOverlayState(); -} - -class _ZefyrSelectionOverlayState extends State - implements TextSelectionDelegate { - @override - TextEditingValue get textEditingValue => - widget.controller.plainTextEditingValue; - - set textEditingValue(TextEditingValue value) { - final cursorPosition = value.selection.extentOffset; - final oldText = widget.controller.document.toPlainText(); - final newText = value.text; - final diff = fastDiff(oldText, newText, cursorPosition); - widget.controller.replaceText( - diff.start, diff.deleted.length, diff.inserted, - selection: value.selection); - } - - @override - void bringIntoView(ui.TextPosition position) { - // TODO: implement bringIntoView - } - - bool get isToolbarVisible => _toolbar != null; - bool get isToolbarHidden => _toolbar == null; - - @override - void hideToolbar() { - _didCaretTap = false; // reset double tap. - _toolbar?.remove(); - _toolbar = null; - _toolbarController.stop(); - } - - void showToolbar() { - final scope = ZefyrScope.of(context); - assert(scope != null); - final toolbarOpacity = _toolbarController.view; - _toolbar = new OverlayEntry( - builder: (context) => new FadeTransition( - opacity: toolbarOpacity, - child: new _SelectionToolbar( - scope: scope, - controls: widget.controls, - delegate: this, - ), - ), - ); - widget.overlay.insert(_toolbar); - _toolbarController.forward(from: 0.0); - } - - // - // Overridden members of State - // - - @override - void initState() { - super.initState(); - _toolbarController = new AnimationController( - duration: _kFadeDuration, vsync: widget.overlay); - } - - static const Duration _kFadeDuration = const Duration(milliseconds: 150); - - @override - void didUpdateWidget(ZefyrSelectionOverlay oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.overlay != widget.overlay) { - hideToolbar(); - _toolbarController.dispose(); - _toolbarController = new AnimationController( - duration: _kFadeDuration, vsync: widget.overlay); - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final editor = ZefyrScope.of(context); - if (_editor != editor) { - _editor?.removeListener(_handleChange); - _editor = editor; - _editor.addListener(_handleChange); - _selection = _editor.selection; - _focusOwner = _editor.focusOwner; - } - } - - @override - void dispose() { - _editor.removeListener(_handleChange); - hideToolbar(); - _toolbarController.dispose(); - _toolbarController = null; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final overlay = new GestureDetector( - behavior: HitTestBehavior.translucent, - onTapDown: _handleTapDown, - onTap: _handleTap, - onTapCancel: _handleTapCancel, - onLongPress: _handleLongPress, - child: new Stack( - fit: StackFit.expand, - children: [ - new SelectionHandleDriver( - position: _SelectionHandlePosition.base, - controls: widget.controls, - ), - new SelectionHandleDriver( - position: _SelectionHandlePosition.extent, - controls: widget.controls, - ), - ], - ), - ); - return new Container(child: overlay); - } - - // - // Private members - // - - /// Global position of last TapDown event. - Offset _lastTapDownPosition; - - /// Global position of last TapDown which is potentially a long press. - Offset _longPressPosition; - - OverlayEntry _toolbar; - AnimationController _toolbarController; - - ZefyrScope _editor; - TextSelection _selection; - FocusOwner _focusOwner; - - bool _didCaretTap = false; - - void _handleChange() { - if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) { - _updateToolbar(); - } - } - - void _updateToolbar() { - if (!mounted) { - return; - } - - final selection = _editor.selection; - final focusOwner = _editor.focusOwner; - setState(() { - if (focusOwner != FocusOwner.editor) { - hideToolbar(); - } else { - if (_selection != selection) { - if (selection.isCollapsed && isToolbarVisible) hideToolbar(); - _toolbar?.markNeedsBuild(); - if (!selection.isCollapsed && isToolbarHidden) showToolbar(); - } else { - if (!selection.isCollapsed && isToolbarHidden) { - showToolbar(); - } else if (isToolbarVisible) { - _toolbar?.markNeedsBuild(); - } - } - } - _selection = selection; - _focusOwner = focusOwner; - }); - } - - void _handleTapDown(TapDownDetails details) { - _lastTapDownPosition = details.globalPosition; - } - - void _handleTapCancel() { - // longPress arrives after tapCancel, so remember the tap position. - _longPressPosition = _lastTapDownPosition; - _lastTapDownPosition = null; - } - - void _handleTap() { - assert(_lastTapDownPosition != null); - final globalPoint = _lastTapDownPosition; - _lastTapDownPosition = null; - HitTestResult result = new HitTestResult(); - WidgetsBinding.instance.hitTest(result, globalPoint); - - RenderEditableProxyBox box = _getEditableBox(result); - if (box == null) { - box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint); - } - if (box == null) return null; - - final localPoint = box.globalToLocal(globalPoint); - final position = box.getPositionForOffset(localPoint); - final selection = new TextSelection.collapsed( - offset: position.offset, - affinity: position.affinity, - ); - if (_didCaretTap && _selection == selection) { - _didCaretTap = false; - if (isToolbarVisible) { - hideToolbar(); - } else { - showToolbar(); - } - } else { - _didCaretTap = true; - } - widget.controller.updateSelection(selection, source: ChangeSource.local); - } - - void _handleLongPress() { - final Offset globalPoint = _longPressPosition; - _longPressPosition = null; - HitTestResult result = new HitTestResult(); - WidgetsBinding.instance.hitTest(result, globalPoint); - final box = _getEditableBox(result); - if (box == null) { - return; - } - final localPoint = box.globalToLocal(globalPoint); - final position = box.getPositionForOffset(localPoint); - final word = box.getWordBoundary(position); - final selection = new TextSelection( - baseOffset: word.start, - extentOffset: word.end, - ); - widget.controller.updateSelection(selection, source: ChangeSource.local); - } - - @override - bool get copyEnabled => true; - - @override - bool get cutEnabled => true; - - @override - bool get pasteEnabled => true; - - @override - bool get selectAllEnabled => true; -} - -enum _SelectionHandlePosition { base, extent } - -class SelectionHandleDriver extends StatefulWidget { - const SelectionHandleDriver({ - Key key, - @required this.position, - @required this.controls, - }) : super(key: key); - - final _SelectionHandlePosition position; - final TextSelectionControls controls; - - @override - _SelectionHandleDriverState createState() => - new _SelectionHandleDriverState(); -} - -class _SelectionHandleDriverState extends State { - ZefyrScope _scope; - - /// Current document selection. - TextSelection get selection => _selection; - TextSelection _selection; - - /// Returns `true` if this handle is located at the baseOffset of selection. - bool get isBaseHandle => widget.position == _SelectionHandlePosition.base; - - /// Character offset of this handle in the document. - /// - /// For base handle this equals to [TextSelection.baseOffset] and for - /// extent handle - [TextSelection.extentOffset]. - int get documentOffset => - isBaseHandle ? selection.baseOffset : selection.extentOffset; - - /// Position in pixels of this selection handle within its paragraph [block]. - Offset getPosition(RenderEditableBox block) { - if (block == null) return null; - - final localSelection = block.getLocalSelection(selection); - assert(localSelection != null); - - final boxes = block.getEndpointsForSelection(selection); - assert(boxes.isNotEmpty, 'Got empty boxes for selection ${selection}'); - - final box = isBaseHandle ? boxes.first : boxes.last; - final dx = isBaseHandle ? box.start : box.end; - return new Offset(dx, box.bottom); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final scope = ZefyrScope.of(context); - if (_scope != scope) { - _scope?.removeListener(_handleScopeChange); - _scope = scope; - _scope.addListener(_handleScopeChange); - } - _selection = _scope.selection; - } - - @override - void dispose() { - _scope?.removeListener(_handleScopeChange); - super.dispose(); - } - - // - // Overridden members - // - - @override - Widget build(BuildContext context) { - if (selection == null || - selection.isCollapsed || - widget.controls == null || - _scope.focusOwner != FocusOwner.editor) { - return new Container(); - } - final block = _scope.renderContext.boxForTextOffset(documentOffset); - final position = getPosition(block); - Widget handle; - if (position == null) { - handle = new Container(); - } else { - final handleType = isBaseHandle - ? TextSelectionHandleType.left - : TextSelectionHandleType.right; - handle = new Positioned( - left: position.dx, - top: position.dy, - child: widget.controls.buildHandle( - context, - handleType, - block.preferredLineHeight, - ), - ); - handle = new CompositedTransformFollower( - link: block.layerLink, - showWhenUnlinked: false, - child: new Stack( - overflow: Overflow.visible, - children: [handle], - ), - ); - } - // Always return this gesture detector even if handle is an empty container - // This way we prevent drag gesture from being canceled in case current - // position is somewhere outside of any visible paragraph block. - return new GestureDetector( - onPanStart: _handleDragStart, - onPanUpdate: _handleDragUpdate, - child: handle, - ); - } - - // - // Private members - // - - Offset _dragPosition; - - void _handleScopeChange() { - if (_selection != _scope.selection) { - setState(() { - _selection = _scope.selection; - }); - } - } - - void _handleDragStart(DragStartDetails details) { - _dragPosition = details.globalPosition; - } - - void _handleDragUpdate(DragUpdateDetails details) { - _dragPosition += details.delta; - final globalPoint = _dragPosition; - final paragraph = _scope.renderContext.boxForGlobalPoint(globalPoint); - if (paragraph == null) { - return; - } - - final localPoint = paragraph.globalToLocal(globalPoint); - final position = paragraph.getPositionForOffset(localPoint); - final newSelection = selection.copyWith( - baseOffset: isBaseHandle ? position.offset : selection.baseOffset, - extentOffset: isBaseHandle ? selection.extentOffset : position.offset, - ); - if (newSelection.baseOffset >= newSelection.extentOffset) { - // Don't allow reversed or collapsed selection. - return; - } - - if (newSelection != _selection) { - _scope.updateSelection(newSelection, source: ChangeSource.local); - } - } -} - -class _SelectionToolbar extends StatefulWidget { - const _SelectionToolbar({ - Key key, - @required this.scope, - @required this.controls, - @required this.delegate, - }) : super(key: key); - - final ZefyrScope scope; - final TextSelectionControls controls; - final TextSelectionDelegate delegate; - - @override - _SelectionToolbarState createState() => new _SelectionToolbarState(); -} - -class _SelectionToolbarState extends State<_SelectionToolbar> { - ZefyrScope get editable => widget.scope; - TextSelection get selection => widget.delegate.textEditingValue.selection; - - @override - Widget build(BuildContext context) { - return _buildToolbar(context); - } - - Widget _buildToolbar(BuildContext context) { - final base = selection.baseOffset; - // TODO: Editable is not refreshed and may contain stale renderContext instance. - final block = editable.renderContext.boxForTextOffset(base); - if (block == null) { - return Container(); - } - final boxes = block.getEndpointsForSelection(selection); - // Find the horizontal midpoint, just above the selected text. - final Offset midpoint = new Offset( - (boxes.length == 1) - ? (boxes[0].start + boxes[0].end) / 2.0 - : (boxes[0].start + boxes[1].start) / 2.0, - boxes[0].bottom - block.preferredLineHeight, - ); - - final Rect editingRegion = new Rect.fromPoints( - block.localToGlobal(Offset.zero), - block.localToGlobal(block.size.bottomRight(Offset.zero)), - ); -// final toolbar = widget.controls -// .buildToolbar(context, editingRegion, midpoint, widget.delegate); - final Offset endpoint = new Offset( - (boxes.length == 1) - ? (boxes[0].start + boxes[0].end) - : (boxes[0].start + boxes[1].start), - boxes[0].bottom - block.preferredLineHeight, - ); - final TextSelectionPoint textEndpoint = new TextSelectionPoint(endpoint, TextDirection.ltr); - final toolbar = widget.controls - .buildToolbar(context, editingRegion,0.0, midpoint, [textEndpoint], widget.delegate); - return new CompositedTransformFollower( - link: block.layerLink, - showWhenUnlinked: false, - offset: -editingRegion.topLeft, - child: toolbar, - ); - } -} diff --git a/zefyr/lib/src/widgets/theme.dart b/zefyr/lib/src/widgets/theme.dart deleted file mode 100644 index 0f7c1160..00000000 --- a/zefyr/lib/src/widgets/theme.dart +++ /dev/null @@ -1,312 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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'; - -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; - -/// Applies a Zefyr editor theme to descendant widgets. -/// -/// Describes colors and typographic styles for an editor. -/// -/// Descendant widgets obtain the current theme's [ZefyrThemeData] object using -/// [ZefyrTheme.of]. -/// -/// See also: -/// -/// * [ZefyrThemeData], which describes actual configuration of a theme. -class ZefyrTheme extends InheritedWidget { - final ZefyrThemeData data; - - /// Applies the given theme [data] to [child]. - /// - /// The [data] and [child] arguments must not be null. - ZefyrTheme({ - Key key, - @required this.data, - @required Widget child, - }) : assert(data != null), - assert(child != null), - super(key: key, child: child); - - @override - bool updateShouldNotify(ZefyrTheme oldWidget) { - return data != oldWidget.data; - } - - /// The data from the closest [ZefyrTheme] instance that encloses the given - /// context. - /// - /// Returns `null` if there is no [ZefyrTheme] in the given build context - /// and [nullOk] is set to `true`. If [nullOk] is set to `false` (default) - /// then this method asserts. - static ZefyrThemeData of(BuildContext context, {bool nullOk: false}) { - final ZefyrTheme widget = context.inheritFromWidgetOfExactType(ZefyrTheme); - if (widget == null && nullOk) return null; - assert(widget != null, - '$ZefyrTheme.of() called with a context that does not contain a ZefyrEditor.'); - return widget.data; - } -} - -/// Holds colors and typography styles for [ZefyrEditor]. -class ZefyrThemeData { - final TextStyle boldStyle; - final TextStyle italicStyle; - final TextStyle linkStyle; - final StyleTheme paragraphTheme; - final HeadingTheme headingTheme; - final BlockTheme blockTheme; - final Color selectionColor; - final Color cursorColor; - - /// Size of indentation for blocks. - final double indentSize; - final ZefyrToolbarTheme toolbarTheme; - - factory ZefyrThemeData.fallback(BuildContext context) { - final defaultStyle = DefaultTextStyle.of(context); - final paragraphStyle = defaultStyle.style.copyWith( - fontSize: 16.0, - height: 1.25, - fontWeight: FontWeight.normal, - color: Colors.grey.shade800, - ); - final padding = const EdgeInsets.only(bottom: 16.0); - final boldStyle = new TextStyle(fontWeight: FontWeight.bold); - final italicStyle = new TextStyle(fontStyle: FontStyle.italic); - final linkStyle = - TextStyle(color: Colors.blue, decoration: TextDecoration.underline); - - return new ZefyrThemeData( - boldStyle: boldStyle, - italicStyle: italicStyle, - linkStyle: linkStyle, - paragraphTheme: - new StyleTheme(textStyle: paragraphStyle, padding: padding), - headingTheme: new HeadingTheme.fallback(), - blockTheme: new BlockTheme.fallback(), - selectionColor: Colors.lightBlueAccent.shade100, - cursorColor: Colors.black, - indentSize: 16.0, - toolbarTheme: new ZefyrToolbarTheme.fallback(context), - ); - } - - const ZefyrThemeData({ - this.boldStyle, - this.italicStyle, - this.linkStyle, - this.paragraphTheme, - this.headingTheme, - this.blockTheme, - this.selectionColor, - this.cursorColor, - this.indentSize, - this.toolbarTheme, - }); - - ZefyrThemeData copyWith({ - TextStyle textStyle, - TextStyle boldStyle, - TextStyle italicStyle, - TextStyle linkStyle, - StyleTheme paragraphTheme, - HeadingTheme headingTheme, - BlockTheme blockTheme, - Color selectionColor, - Color cursorColor, - double indentSize, - ZefyrToolbarTheme toolbarTheme, - }) { - return new ZefyrThemeData( - boldStyle: boldStyle ?? this.boldStyle, - italicStyle: italicStyle ?? this.italicStyle, - linkStyle: linkStyle ?? this.linkStyle, - paragraphTheme: paragraphTheme ?? this.paragraphTheme, - headingTheme: headingTheme ?? this.headingTheme, - blockTheme: blockTheme ?? this.blockTheme, - selectionColor: selectionColor ?? this.selectionColor, - cursorColor: cursorColor ?? this.cursorColor, - indentSize: indentSize ?? this.indentSize, - toolbarTheme: toolbarTheme ?? this.toolbarTheme, - ); - } - - ZefyrThemeData merge(ZefyrThemeData other) { - return copyWith( - boldStyle: other.boldStyle, - italicStyle: other.italicStyle, - linkStyle: other.linkStyle, - paragraphTheme: other.paragraphTheme, - headingTheme: other.headingTheme, - blockTheme: other.blockTheme, - selectionColor: other.selectionColor, - cursorColor: other.cursorColor, - indentSize: other.indentSize, - toolbarTheme: other.toolbarTheme, - ); - } -} - -/// Theme for heading-styled lines of text. -class HeadingTheme { - /// Style theme for level 1 headings. - final StyleTheme level1; - - /// Style theme for level 2 headings. - final StyleTheme level2; - - /// Style theme for level 3 headings. - final StyleTheme level3; - - HeadingTheme({ - @required this.level1, - @required this.level2, - @required this.level3, - }); - - /// Creates fallback theme for headings. - factory HeadingTheme.fallback() { - return HeadingTheme( - level1: StyleTheme( - textStyle: TextStyle( - fontSize: 30.0, - color: Colors.grey.shade800, - height: 1.25, - fontWeight: FontWeight.w600, - ), - padding: EdgeInsets.only(top: 16.0, bottom: 16.0), - ), - level2: StyleTheme( - textStyle: TextStyle( - fontSize: 24.0, - color: Colors.grey.shade800, - height: 1.25, - fontWeight: FontWeight.w600, - ), - padding: EdgeInsets.only(bottom: 8.0, top: 8.0), - ), - level3: StyleTheme( - textStyle: TextStyle( - fontSize: 20.0, - color: Colors.grey.shade800, - height: 1.25, - fontWeight: FontWeight.w600, - ), - padding: EdgeInsets.only(bottom: 8.0, top: 8.0), - ), - ); - } -} - -/// Theme for a block of lines in a document. -class BlockTheme { - /// Style theme for bullet lists. - final StyleTheme bulletList; - - /// Style theme for number lists. - final StyleTheme numberList; - - /// Style theme for code snippets. - final StyleTheme code; - - /// Style theme for quotes. - final StyleTheme quote; - - BlockTheme({ - @required this.bulletList, - @required this.numberList, - @required this.quote, - @required this.code, - }); - - /// Creates fallback theme for blocks. - factory BlockTheme.fallback() { - final padding = const EdgeInsets.only(bottom: 8.0); - return new BlockTheme( - bulletList: new StyleTheme(padding: padding), - numberList: new StyleTheme(padding: padding), - quote: new StyleTheme( - textStyle: new TextStyle(color: Colors.grey.shade700), - padding: padding, - ), - code: new StyleTheme( - textStyle: new TextStyle( - color: Colors.blueGrey.shade800, - fontFamily: Platform.isIOS ? 'Menlo' : 'Roboto Mono', - fontSize: 14.0, - height: 1.25, - ), - padding: padding, - ), - ); - } -} - -/// Theme for a specific attribute style. -/// -/// Used in [HeadingTheme] and [BlockTheme], as well as in -/// [ZefyrThemeData.paragraphTheme]. -class StyleTheme { - /// Text style of this theme. - final TextStyle textStyle; - - /// Padding to apply around lines of text. - final EdgeInsets padding; - - /// Creates a new [StyleTheme]. - StyleTheme({ - this.textStyle, - this.padding, - }); -} - -/// Defines styles and colors for [ZefyrToolbar]. -class ZefyrToolbarTheme { - /// The background color of toolbar. - final Color color; - - /// Color of buttons in toggled state. - final Color toggleColor; - - /// Color of button icons. - final Color iconColor; - - /// Color of button icons in disabled state. - final Color disabledIconColor; - - /// Creates fallback theme for editor toolbars. - factory ZefyrToolbarTheme.fallback(BuildContext context) { - final theme = Theme.of(context); - return ZefyrToolbarTheme._( - color: theme.primaryColorLight, - toggleColor: theme.primaryColor, - iconColor: theme.primaryIconTheme.color, - disabledIconColor: theme.primaryColor, - ); - } - - ZefyrToolbarTheme._({ - @required this.color, - @required this.toggleColor, - @required this.iconColor, - @required this.disabledIconColor, - }); - - ZefyrToolbarTheme copyWith({ - Color color, - Color toggleColor, - Color iconColor, - Color disabledIconColor, - }) { - return ZefyrToolbarTheme._( - color: color ?? this.color, - toggleColor: toggleColor ?? this.toggleColor, - iconColor: iconColor ?? this.iconColor, - disabledIconColor: disabledIconColor ?? this.disabledIconColor, - ); - } -} diff --git a/zefyr/lib/src/widgets/toolbar.dart b/zefyr/lib/src/widgets/toolbar.dart deleted file mode 100644 index d9364cbf..00000000 --- a/zefyr/lib/src/widgets/toolbar.dart +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. 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:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:notus/notus.dart'; - -import 'buttons.dart'; -import 'scope.dart'; -import 'theme.dart'; - -/// List of all button actions supported by [ZefyrToolbar] buttons. -enum ZefyrToolbarAction { - bold, - italic, - link, - unlink, - clipboardCopy, - openInBrowser, - heading, - headingLevel1, - headingLevel2, - headingLevel3, - bulletList, - numberList, - code, - quote, - horizontalRule, - image, - cameraImage, - galleryImage, - hideKeyboard, - close, - confirm, -} - -final kZefyrToolbarAttributeActions = { - ZefyrToolbarAction.bold: NotusAttribute.bold, - ZefyrToolbarAction.italic: NotusAttribute.italic, - ZefyrToolbarAction.link: NotusAttribute.link, - ZefyrToolbarAction.heading: NotusAttribute.heading, - ZefyrToolbarAction.headingLevel1: NotusAttribute.heading.level1, - ZefyrToolbarAction.headingLevel2: NotusAttribute.heading.level2, - ZefyrToolbarAction.headingLevel3: NotusAttribute.heading.level3, - ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList, - ZefyrToolbarAction.numberList: NotusAttribute.block.numberList, - ZefyrToolbarAction.code: NotusAttribute.block.code, - ZefyrToolbarAction.quote: NotusAttribute.block.quote, - ZefyrToolbarAction.horizontalRule: NotusAttribute.embed.horizontalRule, -}; - -/// Allows customizing appearance of [ZefyrToolbar]. -abstract class ZefyrToolbarDelegate { - /// Builds toolbar button for specified [action]. - /// - /// Returned widget is usually an instance of [ZefyrButton]. - Widget buildButton(BuildContext context, ZefyrToolbarAction action, - {VoidCallback onPressed}); -} - -/// Scaffold for [ZefyrToolbar]. -class ZefyrToolbarScaffold extends StatelessWidget { - const ZefyrToolbarScaffold({ - Key key, - @required this.body, - this.trailing, - this.autoImplyTrailing: true, - }) : super(key: key); - - final Widget body; - final Widget trailing; - final bool autoImplyTrailing; - - @override - Widget build(BuildContext context) { - final theme = ZefyrTheme.of(context).toolbarTheme; - final toolbar = ZefyrToolbar.of(context); - final constraints = - BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight); - final children = [ - Expanded(child: body), - ]; - - if (trailing != null) { - children.add(trailing); - } else if (autoImplyTrailing) { - children.add(toolbar.buildButton(context, ZefyrToolbarAction.close)); - } - return new Container( - constraints: constraints, - child: Material(color: theme.color, child: Row(children: children)), - ); - } -} - -/// Toolbar for [ZefyrEditor]. -class ZefyrToolbar extends StatefulWidget implements PreferredSizeWidget { - static const kToolbarHeight = 50.0; - - const ZefyrToolbar({ - Key key, - @required this.editor, - this.autoHide: true, - this.delegate, - }) : super(key: key); - - final ZefyrToolbarDelegate delegate; - final ZefyrScope editor; - - /// Whether to automatically hide this toolbar when editor loses focus. - final bool autoHide; - - static ZefyrToolbarState of(BuildContext context) { - final _ZefyrToolbarScope scope = - context.inheritFromWidgetOfExactType(_ZefyrToolbarScope); - return scope?.toolbar; - } - - @override - ZefyrToolbarState createState() => ZefyrToolbarState(); - - @override - ui.Size get preferredSize => new Size.fromHeight(ZefyrToolbar.kToolbarHeight); -} - -class _ZefyrToolbarScope extends InheritedWidget { - _ZefyrToolbarScope({Key key, @required Widget child, @required this.toolbar}) - : super(key: key, child: child); - - final ZefyrToolbarState toolbar; - - @override - bool updateShouldNotify(_ZefyrToolbarScope oldWidget) { - return toolbar != oldWidget.toolbar; - } -} - -class ZefyrToolbarState extends State - with SingleTickerProviderStateMixin { - final Key _toolbarKey = UniqueKey(); - final Key _overlayKey = UniqueKey(); - - ZefyrToolbarDelegate _delegate; - AnimationController _overlayAnimation; - WidgetBuilder _overlayBuilder; - Completer _overlayCompleter; - - TextSelection _selection; - - void markNeedsRebuild() { - setState(() { - if (_selection != editor.selection) { - _selection = editor.selection; - closeOverlay(); - } - }); - } - - Widget buildButton(BuildContext context, ZefyrToolbarAction action, - {VoidCallback onPressed}) { - return _delegate.buildButton(context, action, onPressed: onPressed); - } - - Future showOverlay(WidgetBuilder builder) async { - assert(_overlayBuilder == null); - final completer = new Completer(); - setState(() { - _overlayBuilder = builder; - _overlayCompleter = completer; - _overlayAnimation.forward(); - }); - return completer.future; - } - - void closeOverlay() { - if (!hasOverlay) return; - _overlayAnimation.reverse().whenComplete(() { - setState(() { - _overlayBuilder = null; - _overlayCompleter?.complete(); - _overlayCompleter = null; - }); - }); - } - - bool get hasOverlay => _overlayBuilder != null; - - ZefyrScope get editor => widget.editor; - - @override - void initState() { - super.initState(); - _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate(); - _overlayAnimation = new AnimationController( - vsync: this, duration: Duration(milliseconds: 100)); - _selection = editor.selection; - } - - @override - void didUpdateWidget(ZefyrToolbar oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.delegate != oldWidget.delegate) { - _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate(); - } - } - - @override - void dispose() { - _overlayAnimation.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final layers = []; - - // Must set unique key for the toolbar to prevent it from reconstructing - // new state each time we toggle overlay. - final toolbar = ZefyrToolbarScaffold( - key: _toolbarKey, - body: ZefyrButtonList(buttons: _buildButtons(context)), - trailing: buildButton(context, ZefyrToolbarAction.hideKeyboard), - ); - - layers.add(toolbar); - - if (hasOverlay) { - Widget widget = new Builder(builder: _overlayBuilder); - assert(widget != null); - final overlay = FadeTransition( - key: _overlayKey, - opacity: _overlayAnimation, - child: widget, - ); - layers.add(overlay); - } - - final constraints = - BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight); - return _ZefyrToolbarScope( - toolbar: this, - child: Container( - constraints: constraints, - child: Stack(children: layers), - ), - ); - } - - List _buildButtons(BuildContext context) { - final buttons = [ - buildButton(context, ZefyrToolbarAction.bold), - buildButton(context, ZefyrToolbarAction.italic), - LinkButton(), - HeadingButton(), - buildButton(context, ZefyrToolbarAction.bulletList), - buildButton(context, ZefyrToolbarAction.numberList), - buildButton(context, ZefyrToolbarAction.quote), - buildButton(context, ZefyrToolbarAction.code), - buildButton(context, ZefyrToolbarAction.horizontalRule), - ImageButton(), - ]; - return buttons; - } -} - -/// Scrollable list of toolbar buttons. -class ZefyrButtonList extends StatefulWidget { - const ZefyrButtonList({Key key, @required this.buttons}) : super(key: key); - final List buttons; - - @override - _ZefyrButtonListState createState() => _ZefyrButtonListState(); -} - -class _ZefyrButtonListState extends State { - final ScrollController _controller = new ScrollController(); - bool _showLeftArrow = false; - bool _showRightArrow = false; - - @override - void initState() { - super.initState(); - _controller.addListener(_handleScroll); - // Workaround to allow scroll controller attach to our ListView so that - // we can detect if overflow arrows need to be shown on init. - // TODO: find a better way to detect overflow - Timer.run(_handleScroll); - } - - @override - Widget build(BuildContext context) { - final theme = ZefyrTheme.of(context).toolbarTheme; - final color = theme.iconColor; - final list = ListView( - scrollDirection: Axis.horizontal, - controller: _controller, - children: widget.buttons, - physics: ClampingScrollPhysics(), - ); - - final leftArrow = _showLeftArrow - ? Icon(Icons.arrow_left, size: 18.0, color: color) - : null; - final rightArrow = _showRightArrow - ? Icon(Icons.arrow_right, size: 18.0, color: color) - : null; - return Row( - children: [ - SizedBox( - width: 12.0, - height: ZefyrToolbar.kToolbarHeight, - child: Container(child: leftArrow, color: theme.color), - ), - Expanded(child: ClipRect(child: list)), - SizedBox( - width: 12.0, - height: ZefyrToolbar.kToolbarHeight, - child: Container(child: rightArrow, color: theme.color), - ), - ], - ); - } - - void _handleScroll() { - setState(() { - _showLeftArrow = - _controller.position.minScrollExtent != _controller.position.pixels; - _showRightArrow = - _controller.position.maxScrollExtent != _controller.position.pixels; - }); - } -} - -class _DefaultZefyrToolbarDelegate implements ZefyrToolbarDelegate { - static const kDefaultButtonIcons = { - ZefyrToolbarAction.bold: Icons.format_bold, - ZefyrToolbarAction.italic: Icons.format_italic, - ZefyrToolbarAction.link: Icons.link, - ZefyrToolbarAction.unlink: Icons.link_off, - ZefyrToolbarAction.clipboardCopy: Icons.content_copy, - ZefyrToolbarAction.openInBrowser: Icons.open_in_new, - ZefyrToolbarAction.heading: Icons.format_size, - ZefyrToolbarAction.bulletList: Icons.format_list_bulleted, - ZefyrToolbarAction.numberList: Icons.format_list_numbered, - ZefyrToolbarAction.code: Icons.code, - ZefyrToolbarAction.quote: Icons.format_quote, - ZefyrToolbarAction.horizontalRule: Icons.remove, - ZefyrToolbarAction.image: Icons.photo, - ZefyrToolbarAction.cameraImage: Icons.photo_camera, - ZefyrToolbarAction.galleryImage: Icons.photo_library, - ZefyrToolbarAction.hideKeyboard: Icons.keyboard_hide, - ZefyrToolbarAction.close: Icons.close, - ZefyrToolbarAction.confirm: Icons.check, - }; - - static const kSpecialIconSizes = { - ZefyrToolbarAction.unlink: 20.0, - ZefyrToolbarAction.clipboardCopy: 20.0, - ZefyrToolbarAction.openInBrowser: 20.0, - ZefyrToolbarAction.close: 20.0, - ZefyrToolbarAction.confirm: 20.0, - }; - - static const kDefaultButtonTexts = { - ZefyrToolbarAction.headingLevel1: 'H1', - ZefyrToolbarAction.headingLevel2: 'H2', - ZefyrToolbarAction.headingLevel3: 'H3', - }; - - @override - Widget buildButton(BuildContext context, ZefyrToolbarAction action, - {VoidCallback onPressed}) { - final theme = Theme.of(context); - if (kDefaultButtonIcons.containsKey(action)) { - final icon = kDefaultButtonIcons[action]; - final size = kSpecialIconSizes[action]; - return ZefyrButton.icon( - action: action, - icon: icon, - iconSize: size, - onPressed: onPressed, - ); - } else { - final text = kDefaultButtonTexts[action]; - assert(text != null); - final style = theme.textTheme.caption - .copyWith(fontWeight: FontWeight.bold, fontSize: 14.0); - return ZefyrButton.text( - action: action, - text: text, - style: style, - onPressed: onPressed, - ); - } - } -} diff --git a/zefyr/lib/src/widgets/view.dart b/zefyr/lib/src/widgets/view.dart deleted file mode 100644 index d7b7b13e..00000000 --- a/zefyr/lib/src/widgets/view.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:meta/meta.dart'; -import 'package:notus/notus.dart'; - -import 'code.dart'; -import 'common.dart'; -import 'image.dart'; -import 'list.dart'; -import 'paragraph.dart'; -import 'quote.dart'; -import 'scope.dart'; -import 'theme.dart'; - -/// Non-scrollable read-only view of Notus rich text documents. -@experimental -class ZefyrView extends StatefulWidget { - final NotusDocument document; - final ZefyrImageDelegate imageDelegate; - - const ZefyrView({Key key, @required this.document, this.imageDelegate}) - : super(key: key); - - @override - ZefyrViewState createState() => ZefyrViewState(); -} - -class ZefyrViewState extends State { - ZefyrScope _scope; - ZefyrThemeData _themeData; - - ZefyrImageDelegate get imageDelegate => widget.imageDelegate; - - @override - void initState() { - super.initState(); - _scope = ZefyrScope.view(imageDelegate: widget.imageDelegate); - } - - @override - void didUpdateWidget(ZefyrView oldWidget) { - super.didUpdateWidget(oldWidget); - _scope.imageDelegate = widget.imageDelegate; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final parentTheme = ZefyrTheme.of(context, nullOk: true); - final fallbackTheme = ZefyrThemeData.fallback(context); - _themeData = (parentTheme != null) - ? fallbackTheme.merge(parentTheme) - : fallbackTheme; - } - - @override - void dispose() { - _scope.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ZefyrTheme( - data: _themeData, - child: ZefyrScopeAccess( - scope: _scope, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _buildChildren(context), - ), - ), - ); - } - - List _buildChildren(BuildContext context) { - final result = []; - for (var node in widget.document.root.children) { - result.add(_defaultChildBuilder(context, node)); - } - return result; - } - - Widget _defaultChildBuilder(BuildContext context, Node node) { - if (node is LineNode) { - if (node.hasEmbed) { - return new RawZefyrLine(node: node); - } else if (node.style.contains(NotusAttribute.heading)) { - return new ZefyrHeading(node: node); - } - return new ZefyrParagraph(node: node); - } - - final BlockNode block = node; - final blockStyle = block.style.get(NotusAttribute.block); - if (blockStyle == NotusAttribute.block.code) { - return new ZefyrCode(node: block); - } else if (blockStyle == NotusAttribute.block.bulletList) { - return new ZefyrList(node: block); - } else if (blockStyle == NotusAttribute.block.numberList) { - return new ZefyrList(node: block); - } else if (blockStyle == NotusAttribute.block.quote) { - return new ZefyrQuote(node: block); - } - - throw new UnimplementedError('Block format $blockStyle.'); - } -} diff --git a/zefyr/lib/util.dart b/zefyr/lib/util.dart deleted file mode 100644 index 7ef640e8..00000000 --- a/zefyr/lib/util.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -/// Utility functions for Zefyr. -library zefyr.util; - -import 'dart:math' as math; - -import 'package:quill_delta/quill_delta.dart'; - -export 'src/fast_diff.dart'; - -int getPositionDelta(Delta user, Delta actual) { - final userIter = new DeltaIterator(user); - final actualIter = new DeltaIterator(actual); - int diff = 0; - while (userIter.hasNext || actualIter.hasNext) { - num length = math.min(userIter.peekLength(), actualIter.peekLength()); - final userOp = userIter.next(length); - final actualOp = actualIter.next(length); - assert(userOp.length == actualOp.length); - if (userOp.key == actualOp.key) continue; - if (userOp.isInsert && actualOp.isRetain) { - diff -= userOp.length; - } else if (userOp.isDelete && actualOp.isRetain) { - diff += userOp.length; - } else if (userOp.isRetain && actualOp.isInsert) { - if (actualOp.data.startsWith('\n') ) { - // At this point user input reached its end (retain). If a heuristic - // rule inserts a new line we should keep cursor on it's original position. - continue; - } - diff += actualOp.length; - } else { - // TODO: this likely needs to cover more edge cases. - } - } - return diff; -} diff --git a/zefyr/lib/zefyr.dart b/zefyr/lib/zefyr.dart deleted file mode 100644 index 846ba5f5..00000000 --- a/zefyr/lib/zefyr.dart +++ /dev/null @@ -1,22 +0,0 @@ -library zefyr; - -export 'package:notus/notus.dart'; - -export 'src/widgets/buttons.dart' hide HeadingButton, LinkButton; -export 'src/widgets/code.dart'; -export 'src/widgets/common.dart'; -export 'src/widgets/controller.dart'; -export 'src/widgets/editable_text.dart'; -export 'src/widgets/editor.dart'; -export 'src/widgets/field.dart'; -export 'src/widgets/horizontal_rule.dart'; -export 'src/widgets/image.dart'; -export 'src/widgets/list.dart'; -export 'src/widgets/paragraph.dart'; -export 'src/widgets/quote.dart'; -export 'src/widgets/scaffold.dart'; -export 'src/widgets/scope.dart' hide ZefyrScopeAccess; -export 'src/widgets/selection.dart' hide SelectionHandleDriver; -export 'src/widgets/theme.dart'; -export 'src/widgets/toolbar.dart'; -export 'src/widgets/view.dart'; \ No newline at end of file diff --git a/zefyr/pubspec.yaml b/zefyr/pubspec.yaml deleted file mode 100644 index 24625589..00000000 --- a/zefyr/pubspec.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: zefyr -description: A new Flutter package project. -version: 0.0.1 -author: Anatoly Pulyaevskiy -homepage: - -environment: - sdk: ">=2.1.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - collection: ^1.14.6 - url_launcher: ^5.0.0 - image_picker: ^0.5.0 - quill_delta: ^1.0.0-dev.1.0 - notus: ^0.1.0 - meta: ^1.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/zefyr/test/zefyr_test.dart b/zefyr/test/zefyr_test.dart deleted file mode 100644 index f9502254..00000000 --- a/zefyr/test/zefyr_test.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:zefyr/zefyr.dart'; - -void main() { -// test('adds one to input values', () { -// final calculator = Calculator(); -// expect(calculator.addOne(2), 3); -// expect(calculator.addOne(-7), -6); -// expect(calculator.addOne(0), 1); -// expect(() => calculator.addOne(null), throwsNoSuchMethodError); -// }); -}