|
@@ -1,8 +1,10 @@
|
1
|
1
|
// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
|
2
|
2
|
// for details. All rights reserved. Use of this source code is governed by a
|
3
|
3
|
// BSD-style license that can be found in the LICENSE file.
|
|
4
|
+import 'dart:math' as math;
|
4
|
5
|
import 'dart:ui' as ui;
|
5
|
6
|
|
|
7
|
+import 'package:flutter/gestures.dart';
|
6
|
8
|
import 'package:flutter/material.dart';
|
7
|
9
|
import 'package:flutter/rendering.dart';
|
8
|
10
|
import 'package:notus/notus.dart';
|
|
@@ -75,13 +77,13 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
|
75
|
77
|
final toolbarOpacity = _toolbarController.view;
|
76
|
78
|
_toolbar = new OverlayEntry(
|
77
|
79
|
builder: (context) => new FadeTransition(
|
78
|
|
- opacity: toolbarOpacity,
|
79
|
|
- child: new _SelectionToolbar(
|
80
|
|
- scope: _editor,
|
81
|
|
- controls: widget.controls,
|
82
|
|
- delegate: this,
|
83
|
|
- ),
|
84
|
|
- ),
|
|
80
|
+ opacity: toolbarOpacity,
|
|
81
|
+ child: new _SelectionToolbar(
|
|
82
|
+ scope: _editor,
|
|
83
|
+ controls: widget.controls,
|
|
84
|
+ delegate: this,
|
|
85
|
+ ),
|
|
86
|
+ ),
|
85
|
87
|
);
|
86
|
88
|
widget.overlay.insert(_toolbar);
|
87
|
89
|
_toolbarController.forward(from: 0.0);
|
|
@@ -303,7 +305,8 @@ class SelectionHandleDriver extends StatefulWidget {
|
303
|
305
|
new _SelectionHandleDriverState();
|
304
|
306
|
}
|
305
|
307
|
|
306
|
|
-class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
|
|
308
|
+class _SelectionHandleDriverState extends State<SelectionHandleDriver>
|
|
309
|
+ with SingleTickerProviderStateMixin {
|
307
|
310
|
ZefyrScope _scope;
|
308
|
311
|
|
309
|
312
|
/// Current document selection.
|
|
@@ -320,19 +323,18 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
|
320
|
323
|
int get documentOffset =>
|
321
|
324
|
isBaseHandle ? selection.baseOffset : selection.extentOffset;
|
322
|
325
|
|
323
|
|
- /// Position in pixels of this selection handle within its paragraph [block].
|
324
|
|
- Offset getPosition(RenderEditableBox block) {
|
|
326
|
+ List<TextSelectionPoint> getEndpointsForSelection(RenderEditableBox block) {
|
325
|
327
|
if (block == null) return null;
|
326
|
328
|
|
327
|
|
- final localSelection = block.getLocalSelection(selection);
|
328
|
|
- assert(localSelection != null);
|
329
|
|
-
|
330
|
|
- final boxes = block.getEndpointsForSelection(selection);
|
331
|
|
- assert(boxes.isNotEmpty, 'Got empty boxes for selection ${selection}');
|
332
|
|
-
|
333
|
|
- final box = isBaseHandle ? boxes.first : boxes.last;
|
334
|
|
- final dx = isBaseHandle ? box.start : box.end;
|
335
|
|
- return new Offset(dx, box.bottom);
|
|
329
|
+ final Offset paintOffset = Offset.zero;
|
|
330
|
+ final List<ui.TextBox> boxes = block.getEndpointsForSelection(selection);
|
|
331
|
+ final Offset start =
|
|
332
|
+ Offset(boxes.first.start, boxes.first.bottom) + paintOffset;
|
|
333
|
+ final Offset end = Offset(boxes.last.end, boxes.last.bottom) + paintOffset;
|
|
334
|
+ return <TextSelectionPoint>[
|
|
335
|
+ TextSelectionPoint(start, boxes.first.direction),
|
|
336
|
+ TextSelectionPoint(end, boxes.last.direction),
|
|
337
|
+ ];
|
336
|
338
|
}
|
337
|
339
|
|
338
|
340
|
@override
|
|
@@ -366,39 +368,97 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
|
366
|
368
|
return new Container();
|
367
|
369
|
}
|
368
|
370
|
final block = _scope.renderContext.boxForTextOffset(documentOffset);
|
369
|
|
- final position = getPosition(block);
|
370
|
|
- Widget handle;
|
371
|
|
- if (position == null) {
|
372
|
|
- handle = new Container();
|
373
|
|
- } else {
|
374
|
|
- final handleType = isBaseHandle
|
375
|
|
- ? TextSelectionHandleType.left
|
376
|
|
- : TextSelectionHandleType.right;
|
377
|
|
- handle = new Positioned(
|
378
|
|
- left: position.dx,
|
379
|
|
- top: position.dy,
|
380
|
|
- child: widget.controls.buildHandle(
|
381
|
|
- context,
|
382
|
|
- handleType,
|
383
|
|
- block.preferredLineHeight,
|
384
|
|
- ),
|
385
|
|
- );
|
386
|
|
- handle = new CompositedTransformFollower(
|
387
|
|
- link: block.layerLink,
|
388
|
|
- showWhenUnlinked: false,
|
389
|
|
- child: new Stack(
|
390
|
|
- overflow: Overflow.visible,
|
391
|
|
- children: <Widget>[handle],
|
392
|
|
- ),
|
393
|
|
- );
|
|
371
|
+ if (block == null) {
|
|
372
|
+ // TODO: For some reason sometimes we get updates when render boxes
|
|
373
|
+ // are in process of rebuilding so we don't have access to them here.
|
|
374
|
+ // As a workaround we just return empty container. There is usually
|
|
375
|
+ // another rebuild right after which "fixes" the view.
|
|
376
|
+ // Example: when toolbar button is toggled changing style of current
|
|
377
|
+ // selection.
|
|
378
|
+ return Container();
|
|
379
|
+ }
|
|
380
|
+
|
|
381
|
+ final List<TextSelectionPoint> endpoints = getEndpointsForSelection(block);
|
|
382
|
+ Offset point;
|
|
383
|
+ TextSelectionHandleType type;
|
|
384
|
+
|
|
385
|
+ switch (widget.position) {
|
|
386
|
+ case _SelectionHandlePosition.base:
|
|
387
|
+ point = endpoints[0].point;
|
|
388
|
+ type = _chooseType(endpoints[0], TextSelectionHandleType.left,
|
|
389
|
+ TextSelectionHandleType.right);
|
|
390
|
+ break;
|
|
391
|
+ case _SelectionHandlePosition.extent:
|
|
392
|
+ // [endpoints] will only contain 1 point for collapsed selections, in
|
|
393
|
+ // which case we shouldn't be building the [end] handle.
|
|
394
|
+ assert(endpoints.length == 2);
|
|
395
|
+ point = endpoints[1].point;
|
|
396
|
+ type = _chooseType(endpoints[1], TextSelectionHandleType.right,
|
|
397
|
+ TextSelectionHandleType.left);
|
|
398
|
+ break;
|
394
|
399
|
}
|
395
|
|
- // Always return this gesture detector even if handle is an empty container
|
396
|
|
- // This way we prevent drag gesture from being canceled in case current
|
397
|
|
- // position is somewhere outside of any visible paragraph block.
|
398
|
|
- return new GestureDetector(
|
399
|
|
- onPanStart: _handleDragStart,
|
400
|
|
- onPanUpdate: _handleDragUpdate,
|
401
|
|
- child: handle,
|
|
400
|
+
|
|
401
|
+ final Size viewport = block.size;
|
|
402
|
+ point = Offset(
|
|
403
|
+ point.dx.clamp(0.0, viewport.width),
|
|
404
|
+ point.dy.clamp(0.0, viewport.height),
|
|
405
|
+ );
|
|
406
|
+
|
|
407
|
+ final Offset handleAnchor = widget.controls.getHandleAnchor(
|
|
408
|
+ type,
|
|
409
|
+ block.preferredLineHeight,
|
|
410
|
+ );
|
|
411
|
+ final Size handleSize = widget.controls.getHandleSize(
|
|
412
|
+ block.preferredLineHeight,
|
|
413
|
+ );
|
|
414
|
+ final Rect handleRect = Rect.fromLTWH(
|
|
415
|
+ // Put handleAnchor on top of point
|
|
416
|
+ point.dx - handleAnchor.dx,
|
|
417
|
+ point.dy - handleAnchor.dy,
|
|
418
|
+ handleSize.width,
|
|
419
|
+ handleSize.height,
|
|
420
|
+ );
|
|
421
|
+
|
|
422
|
+ // Make sure the GestureDetector is big enough to be easily interactive.
|
|
423
|
+ final Rect interactiveRect = handleRect.expandToInclude(
|
|
424
|
+ Rect.fromCircle(
|
|
425
|
+ center: handleRect.center, radius: kMinInteractiveSize / 2),
|
|
426
|
+ );
|
|
427
|
+ final RelativeRect padding = RelativeRect.fromLTRB(
|
|
428
|
+ math.max((interactiveRect.width - handleRect.width) / 2, 0),
|
|
429
|
+ math.max((interactiveRect.height - handleRect.height) / 2, 0),
|
|
430
|
+ math.max((interactiveRect.width - handleRect.width) / 2, 0),
|
|
431
|
+ math.max((interactiveRect.height - handleRect.height) / 2, 0),
|
|
432
|
+ );
|
|
433
|
+
|
|
434
|
+ return CompositedTransformFollower(
|
|
435
|
+ link: block.layerLink,
|
|
436
|
+ offset: interactiveRect.topLeft,
|
|
437
|
+ showWhenUnlinked: false,
|
|
438
|
+ child: Container(
|
|
439
|
+ alignment: Alignment.topLeft,
|
|
440
|
+ width: interactiveRect.width,
|
|
441
|
+ height: interactiveRect.height,
|
|
442
|
+ child: GestureDetector(
|
|
443
|
+ behavior: HitTestBehavior.translucent,
|
|
444
|
+ dragStartBehavior: DragStartBehavior.start,
|
|
445
|
+ onPanStart: _handleDragStart,
|
|
446
|
+ onPanUpdate: _handleDragUpdate,
|
|
447
|
+ child: Padding(
|
|
448
|
+ padding: EdgeInsets.only(
|
|
449
|
+ left: padding.left,
|
|
450
|
+ top: padding.top,
|
|
451
|
+ right: padding.right,
|
|
452
|
+ bottom: padding.bottom,
|
|
453
|
+ ),
|
|
454
|
+ child: widget.controls.buildHandle(
|
|
455
|
+ context,
|
|
456
|
+ type,
|
|
457
|
+ block.preferredLineHeight,
|
|
458
|
+ ),
|
|
459
|
+ ),
|
|
460
|
+ ),
|
|
461
|
+ ),
|
402
|
462
|
);
|
403
|
463
|
}
|
404
|
464
|
|
|
@@ -406,6 +466,23 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
|
406
|
466
|
// Private members
|
407
|
467
|
//
|
408
|
468
|
|
|
469
|
+ TextSelectionHandleType _chooseType(
|
|
470
|
+ TextSelectionPoint endpoint,
|
|
471
|
+ TextSelectionHandleType ltrType,
|
|
472
|
+ TextSelectionHandleType rtlType,
|
|
473
|
+ ) {
|
|
474
|
+ if (selection.isCollapsed) return TextSelectionHandleType.collapsed;
|
|
475
|
+
|
|
476
|
+ assert(endpoint.direction != null);
|
|
477
|
+ switch (endpoint.direction) {
|
|
478
|
+ case TextDirection.ltr:
|
|
479
|
+ return ltrType;
|
|
480
|
+ case TextDirection.rtl:
|
|
481
|
+ return rtlType;
|
|
482
|
+ }
|
|
483
|
+ return null;
|
|
484
|
+ }
|
|
485
|
+
|
409
|
486
|
Offset _dragPosition;
|
410
|
487
|
|
411
|
488
|
void _handleScopeChange() {
|
|
@@ -506,8 +583,9 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
|
506
|
583
|
block.localToGlobal(Offset.zero),
|
507
|
584
|
block.localToGlobal(block.size.bottomRight(Offset.zero)),
|
508
|
585
|
);
|
509
|
|
- final toolbar = widget.controls.buildToolbar(
|
510
|
|
- context, editingRegion, midpoint, endpoints, widget.delegate);
|
|
586
|
+
|
|
587
|
+ final toolbar = widget.controls.buildToolbar(context, editingRegion,
|
|
588
|
+ block.preferredLineHeight, midpoint, endpoints, widget.delegate);
|
511
|
589
|
return new CompositedTransformFollower(
|
512
|
590
|
link: block.layerLink,
|
513
|
591
|
showWhenUnlinked: false,
|