Skip to content

Commit

Permalink
[SuperEditor][Mobile] Add caret customization (Resolves superlistapp#…
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre authored and quaaantumdev committed May 23, 2024
1 parent 6ab2e00 commit cb6f85d
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -353,13 +353,16 @@ class SuperEditorAndroidToolbarFocalPointDocumentLayerBuilder implements SuperEd
class SuperEditorAndroidHandlesDocumentLayerBuilder implements SuperEditorLayerBuilder {
const SuperEditorAndroidHandlesDocumentLayerBuilder({
this.caretColor,
this.caretWidth = 2,
});

/// The (optional) color of the caret (not the drag handle), by default the color
/// defers to the root [SuperEditorAndroidControlsScope], or the app theme if the
/// controls controller has no preference for the color.
final Color? caretColor;

final double caretWidth;

@override
ContentLayerWidget build(BuildContext context, SuperEditorContext editContext) {
if (defaultTargetPlatform != TargetPlatform.android ||
Expand All @@ -379,6 +382,7 @@ class SuperEditorAndroidHandlesDocumentLayerBuilder implements SuperEditorLayerB
const ClearComposingRegionRequest(),
]);
},
caretWidth: caretWidth,
caretColor: caretColor,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1838,9 +1838,16 @@ class SuperEditorIosToolbarFocalPointDocumentLayerBuilder implements SuperEditor
class SuperEditorIosHandlesDocumentLayerBuilder implements SuperEditorLayerBuilder {
const SuperEditorIosHandlesDocumentLayerBuilder({
this.handleColor,
this.caretWidth,
this.handleBallDiameter,
});

final Color? handleColor;
final double? caretWidth;

/// The diameter of the small circle that appears on the top and bottom of
/// expanded iOS text handles.
final double? handleBallDiameter;

@override
ContentLayerWidget build(BuildContext context, SuperEditorContext editContext) {
Expand All @@ -1863,6 +1870,8 @@ class SuperEditorIosHandlesDocumentLayerBuilder implements SuperEditorLayerBuild
handleColor: handleColor ??
SuperEditorIosControlsScope.maybeRootOf(context)?.handleColor ??
Theme.of(context).primaryColor,
caretWidth: caretWidth ?? 2,
handleBallDiameter: handleBallDiameter ?? defaultIosHandleBallDiameter,
shouldCaretBlink: SuperEditorIosControlsScope.rootOf(context).shouldCaretBlink,
floatingCursorController: SuperEditorIosControlsScope.rootOf(context).floatingCursorController,
);
Expand All @@ -1873,3 +1882,7 @@ const defaultIosMagnifierEnterAnimationDuration = Duration(milliseconds: 180);
const defaultIosMagnifierExitAnimationDuration = Duration(milliseconds: 150);
const defaultIosMagnifierAnimationCurve = Curves.easeInOut;
const defaultIosMagnifierSize = Size(133, 96);

/// The diameter of the small circle that appears on the top and bottom of
/// expanded iOS text handles.
const defaultIosHandleBallDiameter = 8.0;
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ class AndroidHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget {
required this.documentLayout,
required this.selection,
required this.changeSelection,
this.caretWidth = 2,
this.caretColor,
this.showDebugPaint = false,
});
Expand All @@ -163,6 +164,8 @@ class AndroidHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget {

final void Function(DocumentSelection?, SelectionChangeType, String selectionReason) changeSelection;

final double caretWidth;

/// Color used to render the Android-style caret (not handles), by default the color
/// is retrieved from the root [SuperEditorAndroidControlsController].
final Color? caretColor;
Expand Down Expand Up @@ -387,7 +390,7 @@ class AndroidControlsDocumentLayerState
left: caret.left,
top: caret.top,
height: caret.height,
width: 2,
width: widget.caretWidth,
child: Leader(
link: _controlsController!.collapsedHandleFocalPoint,
child: ListenableBuilder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ class IosHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget {
required this.selection,
required this.changeSelection,
required this.handleColor,
this.caretWidth = 2,
this.handleBallDiameter = defaultIosHandleBallDiameter,
required this.shouldCaretBlink,
this.floatingCursorController,
this.showDebugPaint = false,
Expand All @@ -520,6 +522,12 @@ class IosHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget {
/// Color the iOS-style text selection drag handles.
final Color handleColor;

final double caretWidth;

/// The diameter of the small circle that appears on the top and bottom of
/// expanded iOS text handles.
final double handleBallDiameter;

/// Whether the caret should blink, whenever the caret is visible.
final ValueListenable<bool> shouldCaretBlink;

Expand All @@ -538,10 +546,6 @@ class IosHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget {
@visibleForTesting
class IosControlsDocumentLayerState extends DocumentLayoutLayerState<IosHandlesDocumentLayer, DocumentSelectionLayout>
with SingleTickerProviderStateMixin {
/// The diameter of the small circle that appears on the top and bottom of
/// expanded iOS text handles.
static const ballDiameter = 8.0;

// These global keys are assigned to each draggable handle to
// prevent a strange dragging issue.
//
Expand Down Expand Up @@ -775,6 +779,7 @@ class IosControlsDocumentLayerState extends DocumentLayoutLayerState<IosHandlesD
controller: _caretBlinkController,
color: isShowingFloatingCursor ? Colors.grey : widget.handleColor,
caretHeight: caret.height,
caretWidth: widget.caretWidth,
);
},
),
Expand All @@ -788,15 +793,16 @@ class IosControlsDocumentLayerState extends DocumentLayoutLayerState<IosHandlesD
return Positioned(
key: _upstreamHandleKey,
left: upstream.left,
top: upstream.top - ballDiameter,
top: upstream.top - widget.handleBallDiameter,
child: FractionalTranslation(
translation: const Offset(-0.5, 0),
child: IOSSelectionHandle.upstream(
key: DocumentKeys.upstreamHandle,
color: widget.handleColor,
handleType: HandleType.upstream,
caretHeight: upstream.height,
ballRadius: ballDiameter / 2,
caretWidth: widget.caretWidth,
ballRadius: widget.handleBallDiameter / 2,
),
),
);
Expand All @@ -817,7 +823,8 @@ class IosControlsDocumentLayerState extends DocumentLayoutLayerState<IosHandlesD
color: widget.handleColor,
handleType: HandleType.downstream,
caretHeight: downstream.height,
ballRadius: ballDiameter / 2,
caretWidth: widget.caretWidth,
ballRadius: widget.handleBallDiameter / 2,
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class IOSSelectionHandle extends StatelessWidget {
),
),
Container(
width: 2,
width: caretWidth,
height: caretHeight + ballRadius,
color: color,
),
Expand Down Expand Up @@ -131,7 +131,7 @@ class IOSCollapsedHandle extends StatelessWidget {
controller: controller,
caretOffset: Offset.zero,
caretHeight: caretHeight,
width: 2,
width: caretWidth,
color: color,
borderRadius: BorderRadius.zero,
isTextEmpty: false,
Expand Down
48 changes: 44 additions & 4 deletions super_editor/test/super_editor/supereditor_test_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,26 @@ class TestSuperEditorConfigurator {
return this;
}

TestSuperEditorConfigurator withIosCaretStyle({
double? width,
Color? color,
double? handleBallDiameter,
}) {
_config.iosCaretWidth = width;
_config.iosHandleColor = color;
_config.iosHandleBallDiameter = handleBallDiameter;
return this;
}

TestSuperEditorConfigurator withAndroidCaretStyle({
double? width,
Color? color,
}) {
_config.androidCaretWidth = width;
_config.androidCaretColor = color;
return this;
}

/// Configures the [SuperEditor]'s [SoftwareKeyboardController].
TestSuperEditorConfigurator withSoftwareKeyboardController(SoftwareKeyboardController controller) {
_config.softwareKeyboardController = controller;
Expand Down Expand Up @@ -562,13 +582,18 @@ class _TestSuperEditorState extends State<_TestSuperEditor> {

List<SuperEditorLayerBuilder> _createOverlayBuilders() {
// We show the default overlays except in the cases where we want to hide the caret
// or use a custom `CaretStyle`. In those case, we don't include the defaults - we provide
// or use a custom caret style. In those case, we don't include the defaults - we provide
// a configured caret overlay builder, instead.
//
// If you introduce further configuration to overlay builders, make sure that in the default
// situation, we're using `defaultSuperEditorDocumentOverlayBuilders`, so that most tests
// verify the defaults that most apps will use.
if (widget.testConfiguration.displayCaretWithExpandedSelection && widget.testConfiguration.caretStyle == null) {
if (widget.testConfiguration.displayCaretWithExpandedSelection &&
widget.testConfiguration.caretStyle == null &&
widget.testConfiguration.iosCaretWidth == null &&
widget.testConfiguration.iosHandleColor == null &&
widget.testConfiguration.iosHandleBallDiameter == null &&
widget.testConfiguration.androidCaretWidth == null) {
return defaultSuperEditorDocumentOverlayBuilders;
}

Expand All @@ -578,13 +603,20 @@ class _TestSuperEditorState extends State<_TestSuperEditor> {
// iOS floating toolbar.
const SuperEditorIosToolbarFocalPointDocumentLayerBuilder(),
// Displays caret and drag handles, specifically for iOS.
const SuperEditorIosHandlesDocumentLayerBuilder(),
SuperEditorIosHandlesDocumentLayerBuilder(
caretWidth: widget.testConfiguration.iosCaretWidth,
handleColor: widget.testConfiguration.iosHandleColor,
handleBallDiameter: widget.testConfiguration.iosHandleBallDiameter,
),

// Adds a Leader around the document selection at a focal point for the
// Android floating toolbar.
const SuperEditorAndroidToolbarFocalPointDocumentLayerBuilder(),
// Displays caret and drag handles, specifically for Android.
const SuperEditorAndroidHandlesDocumentLayerBuilder(),
SuperEditorAndroidHandlesDocumentLayerBuilder(
caretWidth: widget.testConfiguration.androidCaretWidth ?? 2.0,
caretColor: widget.testConfiguration.androidCaretColor,
),

// Displays caret for typical desktop use-cases.
DefaultCaretOverlayBuilder(
Expand Down Expand Up @@ -618,6 +650,14 @@ class SuperEditorTestConfiguration {
SelectionStyles? selectionStyles;
bool displayCaretWithExpandedSelection = true;
CaretStyle? caretStyle;

double? iosCaretWidth;
Color? iosHandleColor;
double? iosHandleBallDiameter;

double? androidCaretWidth;
Color? androidCaretColor;

SoftwareKeyboardController? softwareKeyboardController;
SuperEditorImePolicies? imePolicies;
SuperEditorImeConfiguration? imeConfiguration;
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions super_editor/test_goldens/editor/supereditor_caret_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor/super_editor_test.dart';

import '../../test/super_editor/supereditor_test_tools.dart';
import '../test_tools_goldens.dart';
Expand Down Expand Up @@ -95,6 +96,58 @@ void main() {
// TODO: find out why this test fails on CI only.
skip: true,
);

testGoldensOniOS('allows customizing the caret width', (tester) async {
await tester //
.createDocument()
.withSingleParagraph()
.withIosCaretStyle(width: 4.0)
.pump();

// Place caret at "Lorem ip|sum"
await tester.placeCaretInParagraph('1', 8);

await screenMatchesGolden(tester, 'super-editor-ios-custom-caret-width');
});

testGoldensOniOS('allows customizing the expanded handle width', (tester) async {
await tester //
.createDocument()
.withSingleParagraph()
.withIosCaretStyle(width: 4.0)
.pump();

// Double tap to select the word ipsum.
await tester.doubleTapInParagraph('1', 8);

await screenMatchesGolden(tester, 'super-editor-ios-custom-handle-width');
});

testGoldensOniOS('allows customizing the expanded handle ball diameter', (tester) async {
await tester //
.createDocument()
.withSingleParagraph()
.withIosCaretStyle(handleBallDiameter: 16.0)
.pump();

// Double tap to select the word ipsum.
await tester.doubleTapInParagraph('1', 8);

await screenMatchesGolden(tester, 'super-editor-ios-custom-handle-ball-diameter');
});

testGoldensOnAndroid('allows customizing the caret width', (tester) async {
await tester //
.createDocument()
.withSingleParagraph()
.withAndroidCaretStyle(width: 4)
.pump();

// Place caret at "Lorem ip|sum"
await tester.placeCaretInParagraph('1', 8);

await screenMatchesGolden(tester, 'super-editor-android-custom-caret-width');
});
});
}

Expand Down

0 comments on commit cb6f85d

Please sign in to comment.