|
|
|
@ -15,6 +15,18 @@ import 'replacements.dart';
|
|
|
|
|
typedef SelectionChangedCallback = void Function(
|
|
|
|
|
TextSelection selection, SelectionChangedCause? cause);
|
|
|
|
|
|
|
|
|
|
/// Signature for a widget builder that builds a context menu for the given
|
|
|
|
|
/// editable field.
|
|
|
|
|
typedef BasicTextFieldContextMenuBuilder = Widget Function(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
ClipboardStatus clipboardStatus,
|
|
|
|
|
VoidCallback? onCopy,
|
|
|
|
|
VoidCallback? onCut,
|
|
|
|
|
VoidCallback? onPaste,
|
|
|
|
|
VoidCallback? onSelectAll,
|
|
|
|
|
TextSelectionToolbarAnchors anchors,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// A basic text input client. An implementation of [DeltaTextInputClient] meant to
|
|
|
|
|
/// send/receive information from the framework to the platform's text input plugin
|
|
|
|
|
/// and vice-versa.
|
|
|
|
@ -25,6 +37,7 @@ class BasicTextInputClient extends StatefulWidget {
|
|
|
|
|
required this.style,
|
|
|
|
|
required this.focusNode,
|
|
|
|
|
this.selectionControls,
|
|
|
|
|
this.contextMenuBuilder,
|
|
|
|
|
required this.onSelectionChanged,
|
|
|
|
|
required this.showSelectionHandles,
|
|
|
|
|
});
|
|
|
|
@ -35,6 +48,7 @@ class BasicTextInputClient extends StatefulWidget {
|
|
|
|
|
final TextSelectionControls? selectionControls;
|
|
|
|
|
final bool showSelectionHandles;
|
|
|
|
|
final SelectionChangedCallback onSelectionChanged;
|
|
|
|
|
final BasicTextFieldContextMenuBuilder? contextMenuBuilder;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<BasicTextInputClient> createState() => BasicTextInputClientState();
|
|
|
|
@ -50,6 +64,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
|
|
|
|
widget.focusNode.addListener(_handleFocusChanged);
|
|
|
|
|
widget.controller.addListener(_didChangeTextEditingValue);
|
|
|
|
|
}
|
|
|
|
@ -63,18 +78,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
widget.controller.removeListener(_didChangeTextEditingValue);
|
|
|
|
|
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
|
|
|
|
_clipboardStatus?.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didChangeInputControl(
|
|
|
|
|
TextInputControl? oldControl, TextInputControl? newControl) {
|
|
|
|
|
if (_hasFocus && _hasInputConnection) {
|
|
|
|
|
oldControl?.hide();
|
|
|
|
|
newControl?.show();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// [DeltaTextInputClient] method implementations.
|
|
|
|
|
@override
|
|
|
|
|
void connectionClosed() {
|
|
|
|
@ -94,6 +102,15 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
@override
|
|
|
|
|
TextEditingValue? get currentTextEditingValue => _value;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didChangeInputControl(
|
|
|
|
|
TextInputControl? oldControl, TextInputControl? newControl) {
|
|
|
|
|
if (_hasFocus && _hasInputConnection) {
|
|
|
|
|
oldControl?.hide();
|
|
|
|
|
newControl?.show();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void insertTextPlaceholder(Size size) {
|
|
|
|
|
// Will not implement. This method is used for Scribble support.
|
|
|
|
@ -296,27 +313,15 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _userUpdateTextEditingValueWithDelta(
|
|
|
|
|
TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
|
|
|
|
|
TextEditingValue value = _value;
|
|
|
|
|
|
|
|
|
|
value = textEditingDelta.apply(value);
|
|
|
|
|
|
|
|
|
|
if (widget.controller is ReplacementTextEditingController) {
|
|
|
|
|
(widget.controller as ReplacementTextEditingController)
|
|
|
|
|
.syncReplacementRanges(textEditingDelta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value != _value) {
|
|
|
|
|
manager.updateTextEditingDeltaHistory([textEditingDelta]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userUpdateTextEditingValue(value, cause);
|
|
|
|
|
void _onChangedClipboardStatus() {
|
|
|
|
|
setState(() {
|
|
|
|
|
// Inform the widget that the value of clipboardStatus has changed.
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Keyboard text editing actions.
|
|
|
|
|
// The Handling of the default text editing shortcuts with deltas
|
|
|
|
|
// needs to be in the framework somehow. This should go through some kind of
|
|
|
|
|
// needs to be in the framework somehow. This should go through some kind of
|
|
|
|
|
// generic "replace" method like in EditableText.
|
|
|
|
|
// EditableText converts intents like DeleteCharacterIntent to a generic
|
|
|
|
|
// ReplaceTextIntent. I wonder if that could be done at a higher level, so
|
|
|
|
@ -448,6 +453,24 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _userUpdateTextEditingValueWithDelta(
|
|
|
|
|
TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
|
|
|
|
|
TextEditingValue value = _value;
|
|
|
|
|
|
|
|
|
|
value = textEditingDelta.apply(value);
|
|
|
|
|
|
|
|
|
|
if (widget.controller is ReplacementTextEditingController) {
|
|
|
|
|
(widget.controller as ReplacementTextEditingController)
|
|
|
|
|
.syncReplacementRanges(textEditingDelta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value != _value) {
|
|
|
|
|
manager.updateTextEditingDeltaHistory([textEditingDelta]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userUpdateTextEditingValue(value, cause);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// For updates to text editing value.
|
|
|
|
|
void _didChangeTextEditingValue() {
|
|
|
|
|
_updateRemoteTextEditingValueIfNeeded();
|
|
|
|
@ -455,28 +478,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
setState(() {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _toggleToolbar() {
|
|
|
|
|
assert(_selectionOverlay != null);
|
|
|
|
|
if (_selectionOverlay!.toolbarIsVisible) {
|
|
|
|
|
hideToolbar(false);
|
|
|
|
|
} else {
|
|
|
|
|
showToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When the framework's text editing value changes we should update the text editing
|
|
|
|
|
// value contained within the selection overlay or we might observe unexpected behavior.
|
|
|
|
|
void _updateOrDisposeOfSelectionOverlayIfNeeded() {
|
|
|
|
|
if (_selectionOverlay != null) {
|
|
|
|
|
if (_hasFocus) {
|
|
|
|
|
_selectionOverlay!.update(_value);
|
|
|
|
|
} else {
|
|
|
|
|
_selectionOverlay!.dispose();
|
|
|
|
|
_selectionOverlay = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only update the platform's text input plugin's text editing value when it has changed
|
|
|
|
|
// to avoid sending duplicate update messages to the engine.
|
|
|
|
|
void _updateRemoteTextEditingValueIfNeeded() {
|
|
|
|
@ -488,6 +489,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// For correctly positioning the candidate menu on macOS.
|
|
|
|
|
// Sends the current composing rect to the iOS text input plugin via the text
|
|
|
|
|
// input channel. We need to keep sending the information even if no text is
|
|
|
|
|
// currently marked, as the information usually lags behind. The text input
|
|
|
|
@ -542,6 +544,20 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
// Not implemented.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool get cutEnabled => !textEditingValue.selection.isCollapsed;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool get copyEnabled => !textEditingValue.selection.isCollapsed;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool get pasteEnabled =>
|
|
|
|
|
_clipboardStatus == null ||
|
|
|
|
|
_clipboardStatus!.value == ClipboardStatus.pasteable;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool get selectAllEnabled => textEditingValue.text.isNotEmpty;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void copySelection(SelectionChangedCause cause) {
|
|
|
|
|
final TextSelection copyRange = textEditingValue.selection;
|
|
|
|
@ -599,17 +615,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
_clipboardStatus?.update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void hideToolbar([bool hideHandles = true]) {
|
|
|
|
|
if (hideHandles) {
|
|
|
|
|
// Hide the handles and the toolbar.
|
|
|
|
|
_selectionOverlay?.hide();
|
|
|
|
|
} else if (_selectionOverlay?.toolbarIsVisible ?? false) {
|
|
|
|
|
// Hide only the toolbar but not the handles.
|
|
|
|
|
_selectionOverlay?.hideToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> pasteText(SelectionChangedCause cause) async {
|
|
|
|
|
final TextSelection pasteRange = textEditingValue.selection;
|
|
|
|
@ -649,6 +654,18 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
),
|
|
|
|
|
cause,
|
|
|
|
|
);
|
|
|
|
|
if (cause == SelectionChangedCause.toolbar) {
|
|
|
|
|
switch (defaultTargetPlatform) {
|
|
|
|
|
case TargetPlatform.android:
|
|
|
|
|
case TargetPlatform.iOS:
|
|
|
|
|
case TargetPlatform.fuchsia:
|
|
|
|
|
break;
|
|
|
|
|
case TargetPlatform.macOS:
|
|
|
|
|
case TargetPlatform.linux:
|
|
|
|
|
case TargetPlatform.windows:
|
|
|
|
|
hideToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@ -699,6 +716,17 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void hideToolbar([bool hideHandles = true]) {
|
|
|
|
|
if (hideHandles) {
|
|
|
|
|
// Hide the handles and the toolbar.
|
|
|
|
|
_selectionOverlay?.hide();
|
|
|
|
|
} else if (_selectionOverlay?.toolbarIsVisible ?? false) {
|
|
|
|
|
// Hide only the toolbar but not the handles.
|
|
|
|
|
_selectionOverlay?.hideToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// For TextSelection.
|
|
|
|
|
final LayerLink _startHandleLayerLink = LayerLink();
|
|
|
|
|
final LayerLink _endHandleLayerLink = LayerLink();
|
|
|
|
@ -711,14 +739,14 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
void _handleSelectionChanged(
|
|
|
|
|
TextSelection selection, SelectionChangedCause? cause) {
|
|
|
|
|
// We return early if the selection is not valid. This can happen when the
|
|
|
|
|
// text of [EditableText] is updated at the same time as the selection is
|
|
|
|
|
// text of the editable is updated at the same time as the selection is
|
|
|
|
|
// changed by a gesture event.
|
|
|
|
|
if (!widget.controller.isSelectionWithinTextBounds(selection)) return;
|
|
|
|
|
|
|
|
|
|
widget.controller.selection = selection;
|
|
|
|
|
|
|
|
|
|
// This will show the keyboard for all selection changes on the
|
|
|
|
|
// EditableText except for those triggered by a keyboard input.
|
|
|
|
|
// editable except for those triggered by a keyboard input.
|
|
|
|
|
// Typically BasicTextInputClient shouldn't take user keyboard input if
|
|
|
|
|
// it's not focused already.
|
|
|
|
|
switch (cause) {
|
|
|
|
@ -738,28 +766,12 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (widget.selectionControls == null) {
|
|
|
|
|
if (widget.selectionControls == null && widget.contextMenuBuilder == null) {
|
|
|
|
|
_selectionOverlay?.dispose();
|
|
|
|
|
_selectionOverlay = null;
|
|
|
|
|
} else {
|
|
|
|
|
if (_selectionOverlay == null) {
|
|
|
|
|
_selectionOverlay = TextSelectionOverlay(
|
|
|
|
|
clipboardStatus: _clipboardStatus,
|
|
|
|
|
context: context,
|
|
|
|
|
value: _value,
|
|
|
|
|
debugRequiredFor: widget,
|
|
|
|
|
toolbarLayerLink: _toolbarLayerLink,
|
|
|
|
|
startHandleLayerLink: _startHandleLayerLink,
|
|
|
|
|
endHandleLayerLink: _endHandleLayerLink,
|
|
|
|
|
renderObject: renderEditable,
|
|
|
|
|
selectionControls: widget.selectionControls,
|
|
|
|
|
selectionDelegate: this,
|
|
|
|
|
dragStartBehavior: DragStartBehavior.start,
|
|
|
|
|
onSelectionHandleTapped: () {
|
|
|
|
|
_toggleToolbar();
|
|
|
|
|
},
|
|
|
|
|
magnifierConfiguration: TextMagnifierConfiguration.disabled,
|
|
|
|
|
);
|
|
|
|
|
_selectionOverlay = _createSelectionOverlay();
|
|
|
|
|
} else {
|
|
|
|
|
_selectionOverlay!.update(_value);
|
|
|
|
|
}
|
|
|
|
@ -780,6 +792,136 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TextSelectionOverlay _createSelectionOverlay() {
|
|
|
|
|
final TextSelectionOverlay selectionOverlay = TextSelectionOverlay(
|
|
|
|
|
clipboardStatus: _clipboardStatus,
|
|
|
|
|
context: context,
|
|
|
|
|
value: _value,
|
|
|
|
|
debugRequiredFor: widget,
|
|
|
|
|
toolbarLayerLink: _toolbarLayerLink,
|
|
|
|
|
startHandleLayerLink: _startHandleLayerLink,
|
|
|
|
|
endHandleLayerLink: _endHandleLayerLink,
|
|
|
|
|
renderObject: renderEditable,
|
|
|
|
|
selectionControls: widget.selectionControls,
|
|
|
|
|
selectionDelegate: this,
|
|
|
|
|
dragStartBehavior: DragStartBehavior.start,
|
|
|
|
|
onSelectionHandleTapped: () {
|
|
|
|
|
_toggleToolbar();
|
|
|
|
|
},
|
|
|
|
|
contextMenuBuilder: widget.contextMenuBuilder == null || kIsWeb
|
|
|
|
|
? null
|
|
|
|
|
: (context) {
|
|
|
|
|
return widget.contextMenuBuilder!(
|
|
|
|
|
context,
|
|
|
|
|
_clipboardStatus!.value,
|
|
|
|
|
copyEnabled
|
|
|
|
|
? () => copySelection(SelectionChangedCause.toolbar)
|
|
|
|
|
: null,
|
|
|
|
|
cutEnabled
|
|
|
|
|
? () => cutSelection(SelectionChangedCause.toolbar)
|
|
|
|
|
: null,
|
|
|
|
|
pasteEnabled
|
|
|
|
|
? () => pasteText(SelectionChangedCause.toolbar)
|
|
|
|
|
: null,
|
|
|
|
|
selectAllEnabled
|
|
|
|
|
? () => selectAll(SelectionChangedCause.toolbar)
|
|
|
|
|
: null,
|
|
|
|
|
_contextMenuAnchors,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
magnifierConfiguration: TextMagnifierConfiguration.disabled,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return selectionOverlay;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _toggleToolbar() {
|
|
|
|
|
final TextSelectionOverlay selectionOverlay =
|
|
|
|
|
_selectionOverlay ??= _createSelectionOverlay();
|
|
|
|
|
|
|
|
|
|
if (selectionOverlay.toolbarIsVisible) {
|
|
|
|
|
hideToolbar(false);
|
|
|
|
|
} else {
|
|
|
|
|
showToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When the framework's text editing value changes we should update the text editing
|
|
|
|
|
// value contained within the selection overlay or we might observe unexpected behavior.
|
|
|
|
|
void _updateOrDisposeOfSelectionOverlayIfNeeded() {
|
|
|
|
|
if (_selectionOverlay != null) {
|
|
|
|
|
if (_hasFocus) {
|
|
|
|
|
_selectionOverlay!.update(_value);
|
|
|
|
|
} else {
|
|
|
|
|
_selectionOverlay!.dispose();
|
|
|
|
|
_selectionOverlay = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the line heights at the start and end of the selection for the given
|
|
|
|
|
/// editable.
|
|
|
|
|
_GlyphHeights _getGlyphHeights() {
|
|
|
|
|
final TextSelection selection = textEditingValue.selection;
|
|
|
|
|
|
|
|
|
|
// Only calculate handle rects if the text in the previous frame
|
|
|
|
|
// is the same as the text in the current frame. This is done because
|
|
|
|
|
// widget.renderObject contains the renderEditable from the previous frame.
|
|
|
|
|
// If the text changed between the current and previous frames then
|
|
|
|
|
// widget.renderObject.getRectForComposingRange might fail. In cases where
|
|
|
|
|
// the current frame is different from the previous we fall back to
|
|
|
|
|
// renderObject.preferredLineHeight.
|
|
|
|
|
final InlineSpan span = renderEditable.text!;
|
|
|
|
|
final String prevText = span.toPlainText();
|
|
|
|
|
final String currText = textEditingValue.text;
|
|
|
|
|
if (prevText != currText || !selection.isValid || selection.isCollapsed) {
|
|
|
|
|
return _GlyphHeights(
|
|
|
|
|
start: renderEditable.preferredLineHeight,
|
|
|
|
|
end: renderEditable.preferredLineHeight,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final String selectedGraphemes = selection.textInside(currText);
|
|
|
|
|
final int firstSelectedGraphemeExtent =
|
|
|
|
|
selectedGraphemes.characters.first.length;
|
|
|
|
|
final Rect? startCharacterRect =
|
|
|
|
|
renderEditable.getRectForComposingRange(TextRange(
|
|
|
|
|
start: selection.start,
|
|
|
|
|
end: selection.start + firstSelectedGraphemeExtent,
|
|
|
|
|
));
|
|
|
|
|
final int lastSelectedGraphemeExtent =
|
|
|
|
|
selectedGraphemes.characters.last.length;
|
|
|
|
|
final Rect? endCharacterRect =
|
|
|
|
|
renderEditable.getRectForComposingRange(TextRange(
|
|
|
|
|
start: selection.end - lastSelectedGraphemeExtent,
|
|
|
|
|
end: selection.end,
|
|
|
|
|
));
|
|
|
|
|
return _GlyphHeights(
|
|
|
|
|
start: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
|
|
|
|
end: endCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the anchor points for the default context menu.
|
|
|
|
|
TextSelectionToolbarAnchors get _contextMenuAnchors {
|
|
|
|
|
if (renderEditable.lastSecondaryTapDownPosition != null) {
|
|
|
|
|
return TextSelectionToolbarAnchors(
|
|
|
|
|
primaryAnchor: renderEditable.lastSecondaryTapDownPosition!,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final _GlyphHeights glyphHeights = _getGlyphHeights();
|
|
|
|
|
final TextSelection selection = textEditingValue.selection;
|
|
|
|
|
final List<TextSelectionPoint> points =
|
|
|
|
|
renderEditable.getEndpointsForSelection(selection);
|
|
|
|
|
return TextSelectionToolbarAnchors.fromSelection(
|
|
|
|
|
renderBox: renderEditable,
|
|
|
|
|
startGlyphHeight: glyphHeights.start,
|
|
|
|
|
endGlyphHeight: glyphHeights.end,
|
|
|
|
|
selectionEndpoints: points,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Actions(
|
|
|
|
@ -1018,3 +1160,18 @@ class _Editable extends MultiChildRenderObjectWidget {
|
|
|
|
|
..setPromptRectRange(promptRectRange);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The start and end glyph heights of some range of text.
|
|
|
|
|
@immutable
|
|
|
|
|
class _GlyphHeights {
|
|
|
|
|
const _GlyphHeights({
|
|
|
|
|
required this.start,
|
|
|
|
|
required this.end,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// The glyph height of the first line.
|
|
|
|
|
final double start;
|
|
|
|
|
|
|
|
|
|
/// The glyph height of the last line.
|
|
|
|
|
final double end;
|
|
|
|
|
}
|
|
|
|
|