mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This auto-formats all *.dart files in the repository outside of the `engine` subdirectory and enforces that these files stay formatted with a presubmit check. **Reviewers:** Please carefully review all the commits except for the one titled "formatted". The "formatted" commit was auto-generated by running `dev/tools/format.sh -a -f`. The other commits were hand-crafted to prepare the repo for the formatting change. I recommend reviewing the commits one-by-one via the "Commits" tab and avoiding Github's "Files changed" tab as it will likely slow down your browser because of the size of this PR. --------- Co-authored-by: Kate Lovett <katelovett@google.com> Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
337 lines
11 KiB
Dart
337 lines
11 KiB
Dart
// Copyright 2014 The Flutter Authors. 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/rendering.dart';
|
|
|
|
/// Flutter code sample for [SelectableRegion].
|
|
|
|
void main() => runApp(const SelectableRegionExampleApp());
|
|
|
|
class SelectableRegionExampleApp extends StatelessWidget {
|
|
const SelectableRegionExampleApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
home: SelectableRegion(
|
|
selectionControls: materialTextSelectionControls,
|
|
child: Scaffold(
|
|
appBar: AppBar(title: const Text('SelectableRegion Sample')),
|
|
body: const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
Text('Select this icon', style: TextStyle(fontSize: 30)),
|
|
SizedBox(height: 10),
|
|
MySelectableAdapter(child: Icon(Icons.key, size: 30)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MySelectableAdapter extends StatelessWidget {
|
|
const MySelectableAdapter({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
|
|
if (registrar == null) {
|
|
return child;
|
|
}
|
|
return MouseRegion(
|
|
cursor: SystemMouseCursors.text,
|
|
child: _SelectableAdapter(registrar: registrar, child: child),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SelectableAdapter extends SingleChildRenderObjectWidget {
|
|
const _SelectableAdapter({required this.registrar, required Widget child}) : super(child: child);
|
|
|
|
final SelectionRegistrar registrar;
|
|
|
|
@override
|
|
_RenderSelectableAdapter createRenderObject(BuildContext context) {
|
|
return _RenderSelectableAdapter(DefaultSelectionStyle.of(context).selectionColor!, registrar);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _RenderSelectableAdapter renderObject) {
|
|
renderObject
|
|
..selectionColor = DefaultSelectionStyle.of(context).selectionColor!
|
|
..registrar = registrar;
|
|
}
|
|
}
|
|
|
|
class _RenderSelectableAdapter extends RenderProxyBox with Selectable, SelectionRegistrant {
|
|
_RenderSelectableAdapter(Color selectionColor, SelectionRegistrar registrar)
|
|
: _selectionColor = selectionColor,
|
|
_geometry = ValueNotifier<SelectionGeometry>(_noSelection) {
|
|
this.registrar = registrar;
|
|
_geometry.addListener(markNeedsPaint);
|
|
}
|
|
|
|
static const SelectionGeometry _noSelection = SelectionGeometry(
|
|
status: SelectionStatus.none,
|
|
hasContent: true,
|
|
);
|
|
final ValueNotifier<SelectionGeometry> _geometry;
|
|
|
|
Color get selectionColor => _selectionColor;
|
|
Color _selectionColor;
|
|
set selectionColor(Color value) {
|
|
if (_selectionColor == value) {
|
|
return;
|
|
}
|
|
_selectionColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
// ValueListenable APIs
|
|
|
|
@override
|
|
void addListener(VoidCallback listener) => _geometry.addListener(listener);
|
|
|
|
@override
|
|
void removeListener(VoidCallback listener) => _geometry.removeListener(listener);
|
|
|
|
@override
|
|
SelectionGeometry get value => _geometry.value;
|
|
|
|
// Selectable APIs.
|
|
|
|
@override
|
|
List<Rect> get boundingBoxes => <Rect>[paintBounds];
|
|
|
|
// Adjust this value to enlarge or shrink the selection highlight.
|
|
static const double _padding = 10.0;
|
|
Rect _getSelectionHighlightRect() {
|
|
return Rect.fromLTWH(
|
|
0 - _padding,
|
|
0 - _padding,
|
|
size.width + _padding * 2,
|
|
size.height + _padding * 2,
|
|
);
|
|
}
|
|
|
|
Offset? _start;
|
|
Offset? _end;
|
|
void _updateGeometry() {
|
|
if (_start == null || _end == null) {
|
|
_geometry.value = _noSelection;
|
|
return;
|
|
}
|
|
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
|
|
final Rect selectionRect = Rect.fromPoints(_start!, _end!);
|
|
if (renderObjectRect.intersect(selectionRect).isEmpty) {
|
|
_geometry.value = _noSelection;
|
|
} else {
|
|
final Rect selectionRect = _getSelectionHighlightRect();
|
|
final SelectionPoint firstSelectionPoint = SelectionPoint(
|
|
localPosition: selectionRect.bottomLeft,
|
|
lineHeight: selectionRect.size.height,
|
|
handleType: TextSelectionHandleType.left,
|
|
);
|
|
final SelectionPoint secondSelectionPoint = SelectionPoint(
|
|
localPosition: selectionRect.bottomRight,
|
|
lineHeight: selectionRect.size.height,
|
|
handleType: TextSelectionHandleType.right,
|
|
);
|
|
final bool isReversed;
|
|
if (_start!.dy > _end!.dy) {
|
|
isReversed = true;
|
|
} else if (_start!.dy < _end!.dy) {
|
|
isReversed = false;
|
|
} else {
|
|
isReversed = _start!.dx > _end!.dx;
|
|
}
|
|
_geometry.value = SelectionGeometry(
|
|
status: SelectionStatus.uncollapsed,
|
|
hasContent: true,
|
|
startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint,
|
|
endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint,
|
|
selectionRects: <Rect>[selectionRect],
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
|
|
SelectionResult result = SelectionResult.none;
|
|
switch (event.type) {
|
|
case SelectionEventType.startEdgeUpdate:
|
|
case SelectionEventType.endEdgeUpdate:
|
|
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
|
|
// Normalize offset in case it is out side of the rect.
|
|
final Offset point = globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition);
|
|
final Offset adjustedPoint = SelectionUtils.adjustDragOffset(renderObjectRect, point);
|
|
if (event.type == SelectionEventType.startEdgeUpdate) {
|
|
_start = adjustedPoint;
|
|
} else {
|
|
_end = adjustedPoint;
|
|
}
|
|
result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point);
|
|
case SelectionEventType.clear:
|
|
_start = _end = null;
|
|
case SelectionEventType.selectAll:
|
|
case SelectionEventType.selectWord:
|
|
case SelectionEventType.selectParagraph:
|
|
_start = Offset.zero;
|
|
_end = Offset.infinite;
|
|
case SelectionEventType.granularlyExtendSelection:
|
|
result = SelectionResult.end;
|
|
final GranularlyExtendSelectionEvent extendSelectionEvent =
|
|
event as GranularlyExtendSelectionEvent;
|
|
// Initialize the offset it there is no ongoing selection.
|
|
if (_start == null || _end == null) {
|
|
if (extendSelectionEvent.forward) {
|
|
_start = _end = Offset.zero;
|
|
} else {
|
|
_start = _end = Offset.infinite;
|
|
}
|
|
}
|
|
// Move the corresponding selection edge.
|
|
final Offset newOffset = extendSelectionEvent.forward ? Offset.infinite : Offset.zero;
|
|
if (extendSelectionEvent.isEnd) {
|
|
if (newOffset == _end) {
|
|
result = extendSelectionEvent.forward ? SelectionResult.next : SelectionResult.previous;
|
|
}
|
|
_end = newOffset;
|
|
} else {
|
|
if (newOffset == _start) {
|
|
result = extendSelectionEvent.forward ? SelectionResult.next : SelectionResult.previous;
|
|
}
|
|
_start = newOffset;
|
|
}
|
|
case SelectionEventType.directionallyExtendSelection:
|
|
result = SelectionResult.end;
|
|
final DirectionallyExtendSelectionEvent extendSelectionEvent =
|
|
event as DirectionallyExtendSelectionEvent;
|
|
// Convert to local coordinates.
|
|
final double horizontalBaseLine = globalToLocal(Offset(event.dx, 0)).dx;
|
|
final Offset newOffset;
|
|
final bool forward;
|
|
switch (extendSelectionEvent.direction) {
|
|
case SelectionExtendDirection.backward:
|
|
case SelectionExtendDirection.previousLine:
|
|
forward = false;
|
|
// Initialize the offset it there is no ongoing selection.
|
|
if (_start == null || _end == null) {
|
|
_start = _end = Offset.infinite;
|
|
}
|
|
// Move the corresponding selection edge.
|
|
if (extendSelectionEvent.direction == SelectionExtendDirection.previousLine ||
|
|
horizontalBaseLine < 0) {
|
|
newOffset = Offset.zero;
|
|
} else {
|
|
newOffset = Offset.infinite;
|
|
}
|
|
case SelectionExtendDirection.nextLine:
|
|
case SelectionExtendDirection.forward:
|
|
forward = true;
|
|
// Initialize the offset it there is no ongoing selection.
|
|
if (_start == null || _end == null) {
|
|
_start = _end = Offset.zero;
|
|
}
|
|
// Move the corresponding selection edge.
|
|
if (extendSelectionEvent.direction == SelectionExtendDirection.nextLine ||
|
|
horizontalBaseLine > size.width) {
|
|
newOffset = Offset.infinite;
|
|
} else {
|
|
newOffset = Offset.zero;
|
|
}
|
|
}
|
|
if (extendSelectionEvent.isEnd) {
|
|
if (newOffset == _end) {
|
|
result = forward ? SelectionResult.next : SelectionResult.previous;
|
|
}
|
|
_end = newOffset;
|
|
} else {
|
|
if (newOffset == _start) {
|
|
result = forward ? SelectionResult.next : SelectionResult.previous;
|
|
}
|
|
_start = newOffset;
|
|
}
|
|
}
|
|
_updateGeometry();
|
|
return result;
|
|
}
|
|
|
|
// This method is called when users want to copy selected content in this
|
|
// widget into clipboard.
|
|
@override
|
|
SelectedContent? getSelectedContent() {
|
|
return value.hasSelection ? const SelectedContent(plainText: 'Custom Text') : null;
|
|
}
|
|
|
|
@override
|
|
SelectedContentRange? getSelection() {
|
|
if (!value.hasSelection) {
|
|
return null;
|
|
}
|
|
return const SelectedContentRange(startOffset: 0, endOffset: 1);
|
|
}
|
|
|
|
@override
|
|
int get contentLength => 1;
|
|
|
|
LayerLink? _startHandle;
|
|
LayerLink? _endHandle;
|
|
|
|
@override
|
|
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
|
|
if (_startHandle == startHandle && _endHandle == endHandle) {
|
|
return;
|
|
}
|
|
_startHandle = startHandle;
|
|
_endHandle = endHandle;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
super.paint(context, offset);
|
|
if (!_geometry.value.hasSelection) {
|
|
return;
|
|
}
|
|
// Draw the selection highlight.
|
|
final Paint selectionPaint =
|
|
Paint()
|
|
..style = PaintingStyle.fill
|
|
..color = _selectionColor;
|
|
context.canvas.drawRect(_getSelectionHighlightRect().shift(offset), selectionPaint);
|
|
|
|
// Push the layer links if any.
|
|
if (_startHandle != null) {
|
|
context.pushLayer(
|
|
LeaderLayer(link: _startHandle!, offset: offset + value.startSelectionPoint!.localPosition),
|
|
(PaintingContext context, Offset offset) {},
|
|
Offset.zero,
|
|
);
|
|
}
|
|
if (_endHandle != null) {
|
|
context.pushLayer(
|
|
LeaderLayer(link: _endHandle!, offset: offset + value.endSelectionPoint!.localPosition),
|
|
(PaintingContext context, Offset offset) {},
|
|
Offset.zero,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_geometry.dispose();
|
|
_startHandle = null;
|
|
_endHandle = null;
|
|
super.dispose();
|
|
}
|
|
}
|