Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SuperEditor][Mobile] Add caret customization (Resolves #1935) #2020

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1824,9 +1824,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 @@ -1849,6 +1856,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 @@ -1859,3 +1868,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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I was talking about in my layers comment was meant to go behind the specific place where custom layers were being created. I was also talking about how we expect app developers to customize these properties.

Am I reading this correctly that app developers will need to provide a custom list of layers to SuperEditor if they want a 3px wide caret instead of a 2px wide caret?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I reading this correctly that app developers will need to provide a custom list of layers to SuperEditor if they want a 3px wide caret instead of a 2px wide caret?

Currently yes. We already have caret customization for desktop, which involves creating the whole list of layers...

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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
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
Loading