未验证 提交 2dce4707 编写于 作者: C Christopher Fujino 提交者: GitHub

[flutter_releases] Flutter Stable 2.0.4 Engine Cherrypicks (#25341)

* [web] Fix right click issue when dragging (#24447)
* [web] Fix placeholder-only paragraphs (#24572)
* [web] Reland: Fix painting of last placeholder in paragraph (#24905)
Co-authored-by: NMouad Debbar <mouad.debbar@gmail.com>
上级 3459eb24
repository: https://github.com/flutter/goldens.git
revision: ec80c8042759905a5215ab1cd87ad280e8ef3cd7
revision: 4b4c256d6124a135b70c1a9a7ff10cf2827df31c
......@@ -415,41 +415,43 @@ class _ButtonSanitizer {
);
}
_SanitizedDetails? sanitizeUpEvent() {
_SanitizedDetails? sanitizeMissingRightClickUp({required int buttons}) {
final int newPressedButtons = _htmlButtonsToFlutterButtons(buttons);
// This could happen when RMB is clicked and released but no pointerup
// event was received because context menu was shown.
if (_pressedButtons != 0 && newPressedButtons == 0) {
_pressedButtons = 0;
return _SanitizedDetails(
change: ui.PointerChange.up,
buttons: _pressedButtons,
);
}
return null;
}
_SanitizedDetails? sanitizeUpEvent({required int? buttons}) {
// The pointer could have been released by a `pointerout` event, in which
// case `pointerup` should have no effect.
if (_pressedButtons == 0) {
return null;
}
_pressedButtons = 0;
return _SanitizedDetails(
change: ui.PointerChange.up,
buttons: _pressedButtons,
);
}
_SanitizedDetails? sanitizeUpEventWithButtons({required int buttons}) {
final int newPressedButtons = _htmlButtonsToFlutterButtons(buttons);
// This could happen when the context menu is active and the user clicks
// RMB somewhere else. The browser sends a down event with `buttons:0`.
//
// In this case, we keep the old `buttons` value so we don't confuse the
// framework.
if (_pressedButtons != 0 && newPressedButtons == 0) {
_pressedButtons = _htmlButtonsToFlutterButtons(buttons ?? 0);
if (_pressedButtons == 0) {
// All buttons have been released.
return _SanitizedDetails(
change: ui.PointerChange.up,
buttons: _pressedButtons,
);
} else {
// There are still some unreleased buttons, we shouldn't send an up event
// yet. Instead we send a move event to update the position of the pointer.
return _SanitizedDetails(
change: ui.PointerChange.move,
buttons: _pressedButtons,
);
}
_pressedButtons = newPressedButtons;
return _SanitizedDetails(
change: _pressedButtons == 0
? ui.PointerChange.hover
: ui.PointerChange.move,
buttons: _pressedButtons,
);
}
_SanitizedDetails sanitizeCancelEvent() {
......@@ -462,7 +464,6 @@ class _ButtonSanitizer {
}
typedef _PointerEventListener = dynamic Function(html.PointerEvent event);
const int kContextMenuButton = 2;
/// Adapter class to be used with browsers that support native pointer events.
///
......@@ -512,19 +513,17 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
final int device = event.pointerId!;
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
if (event.button == kContextMenuButton) {
_handleMissingRightMouseUpEvent(sanitizer,
sanitizer._pressedButtons,
sanitizer._pressedButtons & ~kContextMenuButton,
event,
pointerData);
final _SanitizedDetails? up =
sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!);
if (up != null) {
_convertEventsToPointerData(data: pointerData, event: event, details: up);
}
final _SanitizedDetails details =
final _SanitizedDetails down =
sanitizer.sanitizeDownEvent(
button: event.button,
buttons: event.buttons!,
);
_convertEventsToPointerData(data: pointerData, event: event, details: details);
_convertEventsToPointerData(data: pointerData, event: event, details: down);
_callback(pointerData);
});
......@@ -532,21 +531,14 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
final int device = event.pointerId!;
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final int buttonsBeforeEvent = sanitizer._pressedButtons;
final Iterable<_SanitizedDetails> detailsList = _expandEvents(event).map(
(html.PointerEvent expandedEvent) {
return sanitizer.sanitizeMoveEvent(buttons: expandedEvent.buttons!);
},
);
_handleMissingRightMouseUpEvent(
sanitizer,
buttonsBeforeEvent,
(sanitizer._inferDownFlutterButtons(event.button, event.buttons!)
& kContextMenuButton),
event,
pointerData);
for (_SanitizedDetails details in detailsList) {
_convertEventsToPointerData(data: pointerData, event: event, details: details);
final List<html.PointerEvent> expandedEvents = _expandEvents(event);
for (final html.PointerEvent event in expandedEvents) {
final _SanitizedDetails? up = sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!);
if (up != null) {
_convertEventsToPointerData(data: pointerData, event: event, details: up);
}
final _SanitizedDetails move = sanitizer.sanitizeMoveEvent(buttons: event.buttons!);
_convertEventsToPointerData(data: pointerData, event: event, details: move);
}
_callback(pointerData);
}, acceptOutsideGlasspane: true);
......@@ -554,12 +546,12 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_addPointerEventListener('pointerup', (html.PointerEvent event) {
final int device = event.pointerId!;
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final _SanitizedDetails? details = _getSanitizer(device).sanitizeUpEvent();
final _SanitizedDetails? details = _getSanitizer(device).sanitizeUpEvent(buttons: event.buttons);
_removePointerIfUnhoverable(event);
if (details != null) {
_convertEventsToPointerData(data: pointerData, event: event, details: details);
_callback(pointerData);
}
_callback(pointerData);
}, acceptOutsideGlasspane: true);
// A browser fires cancel event if it concludes the pointer will no longer
......@@ -578,39 +570,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
});
}
// Handle special case where right mouse button no longer is pressed.
// We need to synthesize right mouse up, otherwise drag gesture will fail
// to complete or multiple RMB down events will lead to wrong state.
void _handleMissingRightMouseUpEvent(_ButtonSanitizer sanitizer,
int buttonsBeforeEvent, int buttonsAfterEvent, html.PointerEvent event,
List<ui.PointerData> pointerData) {
if ((buttonsBeforeEvent & kContextMenuButton) != 0 &&
buttonsAfterEvent == 0) {
final ui.PointerDeviceKind kind =
_pointerTypeToDeviceKind(event.pointerType!);
final int device = kind == ui.PointerDeviceKind.mouse
? _mouseDeviceId : event.pointerId!;
final double tilt = _computeHighestTilt(event);
final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!);
sanitizer._pressedButtons &= ~kContextMenuButton;
_pointerDataConverter.convert(
pointerData,
change: ui.PointerChange.up,
timeStamp: timeStamp,
kind: kind,
signalKind: ui.PointerSignalKind.none,
device: device,
physicalX: event.client.x.toDouble() * ui.window.devicePixelRatio,
physicalY: event.client.y.toDouble() * ui.window.devicePixelRatio,
buttons: sanitizer._pressedButtons,
pressure: event.pressure as double,
pressureMin: 0.0,
pressureMax: 1.0,
tilt: tilt,
);
}
}
// For each event that is de-coalesced from `event` and described in
// `details`, convert it to pointer data and store in `data`.
void _convertEventsToPointerData({
......@@ -852,12 +811,10 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
void setup() {
_addMouseEventListener('mousedown', (html.MouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
if (event.button == kContextMenuButton) {
_handleMissingRightMouseUpEvent(_sanitizer,
_sanitizer._pressedButtons,
_sanitizer._pressedButtons & ~kContextMenuButton,
event,
pointerData);
final _SanitizedDetails? up =
_sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!);
if (up != null) {
_convertEventsToPointerData(data: pointerData, event: event, details: up);
}
final _SanitizedDetails sanitizedDetails =
_sanitizer.sanitizeDownEvent(
......@@ -870,27 +827,22 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_addMouseEventListener('mousemove', (html.MouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final int buttonsBeforeEvent = _sanitizer._pressedButtons;
_handleMissingRightMouseUpEvent(
_sanitizer,
buttonsBeforeEvent,
(_sanitizer._inferDownFlutterButtons(event.button, event.buttons!)
& kContextMenuButton),
event,
pointerData);
final _SanitizedDetails sanitizedDetails = _sanitizer.sanitizeMoveEvent(buttons: event.buttons!);
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
final _SanitizedDetails? up = _sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!);
if (up != null) {
_convertEventsToPointerData(data: pointerData, event: event, details: up);
}
final _SanitizedDetails move = _sanitizer.sanitizeMoveEvent(buttons: event.buttons!);
_convertEventsToPointerData(data: pointerData, event: event, details: move);
_callback(pointerData);
}, acceptOutsideGlasspane: true);
_addMouseEventListener('mouseup', (html.MouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final bool isEndOfDrag = event.buttons == 0;
final _SanitizedDetails sanitizedDetails = isEndOfDrag ?
_sanitizer.sanitizeUpEvent()! :
_sanitizer.sanitizeUpEventWithButtons(buttons: event.buttons!)!;
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
_callback(pointerData);
final _SanitizedDetails? sanitizedDetails = _sanitizer.sanitizeUpEvent(buttons: event.buttons);
if (sanitizedDetails != null) {
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
_callback(pointerData);
}
}, acceptOutsideGlasspane: true);
_addWheelEventListener((html.Event event) {
......@@ -898,32 +850,6 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
});
}
// Handle special case where right mouse button no longer is pressed.
// We need to synthesize right mouse up, otherwise drag gesture will fail
// to complete or multiple RMB down events will lead to wrong state.
void _handleMissingRightMouseUpEvent(_ButtonSanitizer sanitizer,
int buttonsBeforeEvent, int buttonsAfterEvent, html.MouseEvent event,
List<ui.PointerData> pointerData) {
if ((buttonsBeforeEvent & kContextMenuButton) != 0 &&
buttonsAfterEvent == 0) {
sanitizer._pressedButtons &= ~2;
_pointerDataConverter.convert(
pointerData,
change: ui.PointerChange.up,
timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!),
kind: ui.PointerDeviceKind.mouse,
signalKind: ui.PointerSignalKind.none,
device: _mouseDeviceId,
physicalX: event.client.x.toDouble() * ui.window.devicePixelRatio,
physicalY: event.client.y.toDouble() * ui.window.devicePixelRatio,
buttons: _sanitizer._pressedButtons,
pressure: 1.0,
pressureMin: 0.0,
pressureMax: 1.0,
);
}
}
// For each event that is de-coalesced from `event` and described in
// `detailsList`, convert it to pointer data and store in `data`.
void _convertEventsToPointerData({
......
......@@ -19,8 +19,6 @@ class _PointerState {
_pointer = _pointerCount;
}
bool down = false;
double x;
double y;
}
......@@ -240,8 +238,9 @@ class PointerDataConverter {
double scrollDeltaY = 0.0,
}) {
if (_debugLogPointerConverter) {
print('>> device=$device change = $change buttons = $buttons');
print('>> device=$device change=$change buttons=$buttons');
}
final bool isDown = buttons != 0;
assert(change != null); // ignore: unnecessary_null_comparison
if (signalKind == null ||
signalKind == ui.PointerSignalKind.none) {
......@@ -281,9 +280,8 @@ class PointerDataConverter {
break;
case ui.PointerChange.hover:
final bool alreadyAdded = _pointers.containsKey(device);
final _PointerState state = _ensureStateForPointer(
device, physicalX, physicalY);
assert(!state.down);
_ensureStateForPointer(device, physicalX, physicalY);
assert(!isDown);
if (!alreadyAdded) {
// Synthesizes an add pointer data.
result.add(
......@@ -348,7 +346,7 @@ class PointerDataConverter {
final bool alreadyAdded = _pointers.containsKey(device);
final _PointerState state = _ensureStateForPointer(
device, physicalX, physicalY);
assert(!state.down);
assert(isDown);
state.startNewPointer();
if (!alreadyAdded) {
// Synthesizes an add pointer data.
......@@ -412,7 +410,6 @@ class PointerDataConverter {
)
);
}
state.down = true;
result.add(
_generateCompletePointerData(
timeStamp: timeStamp,
......@@ -445,8 +442,7 @@ class PointerDataConverter {
break;
case ui.PointerChange.move:
assert(_pointers.containsKey(device));
final _PointerState state = _pointers[device]!;
assert(state.down);
assert(isDown);
result.add(
_generateCompletePointerData(
timeStamp: timeStamp,
......@@ -481,7 +477,7 @@ class PointerDataConverter {
case ui.PointerChange.cancel:
assert(_pointers.containsKey(device));
final _PointerState state = _pointers[device]!;
assert(state.down);
assert(!isDown);
// Cancel events can have different coordinates due to various
// reasons (window lost focus which is accompanied by window
// movement, or PointerEvent simply always gives 0). Instead of
......@@ -522,7 +518,6 @@ class PointerDataConverter {
)
);
}
state.down = false;
result.add(
_generateCompletePointerData(
timeStamp: timeStamp,
......@@ -588,7 +583,7 @@ class PointerDataConverter {
case ui.PointerChange.remove:
assert(_pointers.containsKey(device));
final _PointerState state = _pointers[device]!;
assert(!state.down);
assert(!isDown);
result.add(
_generateCompletePointerData(
timeStamp: timeStamp,
......@@ -624,8 +619,7 @@ class PointerDataConverter {
switch (signalKind) {
case ui.PointerSignalKind.scroll:
final bool alreadyAdded = _pointers.containsKey(device);
final _PointerState state = _ensureStateForPointer(
device, physicalX, physicalY);
_ensureStateForPointer(device, physicalX, physicalY);
if (!alreadyAdded) {
// Synthesizes an add pointer data.
result.add(
......@@ -661,7 +655,7 @@ class PointerDataConverter {
// before sending the scroll event, if necessary, so that clients
// don't have to worry about native ordering of hover and scroll
// events.
if (state.down) {
if (isDown) {
result.add(
_synthesizePointerData(
timeStamp: timeStamp,
......
......@@ -78,7 +78,6 @@ class TextLayoutService {
final Spanometer spanometer = Spanometer(paragraph, context);
int spanIndex = 0;
ParagraphSpan span = paragraph.spans[0];
LineBuilder currentLine =
LineBuilder.first(paragraph, spanometer, maxWidth: constraints.width);
......@@ -86,27 +85,34 @@ class TextLayoutService {
// statements (e.g. when we reach `endOfText`, when ellipsis has been
// appended).
while (true) {
// *********************************************** //
// *** HANDLE HARD LINE BREAKS AND END OF TEXT *** //
// *********************************************** //
if (currentLine.end.isHard) {
if (currentLine.isNotEmpty) {
// ************************** //
// *** HANDLE END OF TEXT *** //
// ************************** //
// All spans have been consumed.
final bool reachedEnd = spanIndex == spanCount;
if (reachedEnd) {
// In some cases, we need to extend the line to the end of text and
// build it:
//
// 1. Line is not empty. This could happen when the last span is a
// placeholder.
//
// 2. We haven't reached `LineBreakType.endOfText` yet. This could
// happen when the last character is a new line.
if (currentLine.isNotEmpty || currentLine.end.type != LineBreakType.endOfText) {
currentLine.extendToEndOfText();
lines.add(currentLine.build());
if (currentLine.end.type != LineBreakType.endOfText) {
currentLine = currentLine.nextLine();
}
}
if (currentLine.end.type == LineBreakType.endOfText) {
break;
}
break;
}
// ********************************* //
// *** THE MAIN MEASUREMENT PART *** //
// ********************************* //
final ParagraphSpan span = paragraph.spans[spanIndex];
if (span is PlaceholderSpan) {
if (currentLine.widthIncludingSpace + span.width <= constraints.width) {
// The placeholder fits on the current line.
......@@ -119,6 +125,7 @@ class TextLayoutService {
}
currentLine.addPlaceholder(span);
}
spanIndex++;
} else if (span is FlatTextSpan) {
spanometer.currentSpan = span;
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
......@@ -131,6 +138,10 @@ class TextLayoutService {
// The line can extend to `nextBreak` without overflowing.
currentLine.extendTo(nextBreak);
if (nextBreak.type == LineBreakType.mandatory) {
lines.add(currentLine.build());
currentLine = currentLine.nextLine();
}
} else {
// The chunk of text can't fit into the current line.
final bool isLastLine =
......@@ -158,6 +169,12 @@ class TextLayoutService {
currentLine = currentLine.nextLine();
}
}
// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end) {
currentLine.createBox();
++spanIndex;
}
} else {
throw UnimplementedError('Unknown span type: ${span.runtimeType}');
}
......@@ -165,16 +182,6 @@ class TextLayoutService {
if (lines.length == maxLines) {
break;
}
// ********************************************* //
// *** ADVANCE TO THE NEXT SPAN IF NECESSARY *** //
// ********************************************* //
// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1) {
currentLine.createBox();
span = paragraph.spans[++spanIndex];
}
}
// ************************************************** //
......@@ -198,13 +205,16 @@ class TextLayoutService {
// ******************************** //
spanIndex = 0;
span = paragraph.spans[0];
currentLine =
LineBuilder.first(paragraph, spanometer, maxWidth: constraints.width);
while (currentLine.end.type != LineBreakType.endOfText) {
while (spanIndex < spanCount) {
final ParagraphSpan span = paragraph.spans[spanIndex];
bool breakToNextLine = false;
if (span is PlaceholderSpan) {
currentLine.addPlaceholder(span);
spanIndex++;
} else if (span is FlatTextSpan) {
spanometer.currentSpan = span;
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
......@@ -212,6 +222,16 @@ class TextLayoutService {
// For the purpose of max intrinsic width, we don't care if the line
// fits within the constraints or not. So we always extend it.
currentLine.extendTo(nextBreak);
if (nextBreak.type == LineBreakType.mandatory) {
// We don't want to break the line now because we want to update
// min/max intrinsic widths below first.
breakToNextLine = true;
}
// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end) {
spanIndex++;
}
}
final double widthOfLastSegment = currentLine.lastSegment.width;
......@@ -219,17 +239,13 @@ class TextLayoutService {
minIntrinsicWidth = widthOfLastSegment;
}
if (currentLine.end.isHard) {
// Max intrinsic width includes the width of trailing spaces.
if (maxIntrinsicWidth < currentLine.widthIncludingSpace) {
maxIntrinsicWidth = currentLine.widthIncludingSpace;
}
currentLine = currentLine.nextLine();
// Max intrinsic width includes the width of trailing spaces.
if (maxIntrinsicWidth < currentLine.widthIncludingSpace) {
maxIntrinsicWidth = currentLine.widthIncludingSpace;
}
// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1) {
span = paragraph.spans[++spanIndex];
if (breakToNextLine) {
currentLine = currentLine.nextLine();
}
}
}
......@@ -632,7 +648,10 @@ class LineSegment {
double get widthOfTrailingSpace => widthIncludingSpace - width;
/// Whether this segment is made of only white space.
bool get isSpaceOnly => start.index == end.indexWithoutTrailingSpaces;
///
/// We rely on the [width] to determine this because relying on incides
/// doesn't work well for placeholders (they are zero-length strings).
bool get isSpaceOnly => width == 0;
}
/// Builds instances of [EngineLineMetrics] for the given [paragraph].
......@@ -745,12 +764,19 @@ class LineBuilder {
return widthOfTrailingSpace + spanometer.measure(end, newEnd);
}
bool get _isLastBoxAPlaceholder {
if (_boxes.isEmpty) {
return false;
}
return (_boxes.last is PlaceholderBox);
}
/// Extends the line by setting a [newEnd].
void extendTo(LineBreakResult newEnd) {
// If the current end of the line is a hard break, the line shouldn't be
// extended any further.
assert(
isEmpty || !end.isHard,
isEmpty || !end.isHard || _isLastBoxAPlaceholder,
'Cannot extend a line that ends with a hard break.',
);
......@@ -760,6 +786,28 @@ class LineBuilder {
_addSegment(_createSegment(newEnd));
}
/// Extends the line to the end of the paragraph.
void extendToEndOfText() {
if (end.type == LineBreakType.endOfText) {
return;
}
final LineBreakResult endOfText = LineBreakResult.sameIndex(
paragraph.toPlainText().length,
LineBreakType.endOfText,
);
// The spanometer may not be ready in some cases. E.g. when the paragraph
// is made up of only placeholders and no text.
if (spanometer.isReady) {
ascent = math.max(ascent, spanometer.ascent);
descent = math.max(descent, spanometer.descent);
_addSegment(_createSegment(endOfText));
} else {
end = endOfText;
}
}
void addPlaceholder(PlaceholderSpan placeholder) {
// Increase the line's height to fit the placeholder, if necessary.
final double ascent, descent;
......@@ -1008,7 +1056,7 @@ class LineBuilder {
final LineBreakResult boxEnd = end;
// Avoid creating empty boxes. This could happen when the end of a span
// coincides with the end of a line. In this case, `createBox` is called twice.
if (boxStart == boxEnd) {
if (boxStart.index == boxEnd.index) {
return;
}
......@@ -1029,13 +1077,20 @@ class LineBuilder {
final double ellipsisWidth =
ellipsis == null ? 0.0 : spanometer.measureText(ellipsis);
final int endIndexWithoutNewlines = math.max(start.index, end.indexWithoutTrailingNewlines);
final bool hardBreak;
if (end.type != LineBreakType.endOfText && _isLastBoxAPlaceholder) {
hardBreak = false;
} else {
hardBreak = end.isHard;
}
return EngineLineMetrics.rich(
lineNumber,
ellipsis: ellipsis,
startIndex: start.index,
endIndex: end.index,
endIndexWithoutNewlines: end.indexWithoutTrailingNewlines,
hardBreak: end.isHard,
endIndexWithoutNewlines: endIndexWithoutNewlines,
hardBreak: hardBreak,
width: width + ellipsisWidth,
widthWithTrailingSpaces: widthIncludingSpace + ellipsisWidth,
left: alignOffset,
......@@ -1134,6 +1189,9 @@ class Spanometer {
}
}
/// Whether the spanometer is ready to take measurements.
bool get isReady => _currentSpan != null;
/// The distance from the top of the current span to the alphabetic baseline.
double get ascent => _currentRuler!.alphabeticBaseline;
......
......@@ -6,6 +6,7 @@
import 'dart:html' as html;
import 'dart:js_util' as js_util;
import 'package:meta/meta.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
......@@ -596,18 +597,16 @@ void testMain() {
packets.add(packet);
};
glassPane.dispatchEvent(html.WheelEvent(
'wheel',
button: 1,
glassPane.dispatchEvent(context.wheel(
buttons: 0,
clientX: 10,
clientY: 10,
deltaX: 10,
deltaY: 10,
));
glassPane.dispatchEvent(html.WheelEvent(
'wheel',
button: 1,
glassPane.dispatchEvent(context.wheel(
buttons: 0,
clientX: 20,
clientY: 50,
deltaX: 10,
......@@ -621,9 +620,8 @@ void testMain() {
clientY: 50.0,
));
glassPane.dispatchEvent(html.WheelEvent(
'wheel',
button: 1,
glassPane.dispatchEvent(context.wheel(
buttons: 1,
clientX: 30,
clientY: 60,
deltaX: 10,
......@@ -1485,6 +1483,8 @@ void testMain() {
expect(packets[0].data[0].change, equals(ui.PointerChange.move));
expect(packets[0].data[0].synthesized, equals(true));
expect(packets[0].data[0].buttons, equals(2));
expect(packets[0].data[0].physicalX, equals(20.0 * dpi));
expect(packets[0].data[0].physicalY, equals(20.0 * dpi));
expect(packets[0].data[1].change, equals(ui.PointerChange.up));
expect(packets[0].data[1].synthesized, equals(false));
expect(packets[0].data[1].buttons, equals(0));
......@@ -1661,6 +1661,90 @@ void testMain() {
},
);
_testEach<_ButtonedEventMixin>(
[
_PointerEventContext(),
_MouseEventContext(),
],
'handles overlapping left/right down and up events',
(_ButtonedEventMixin context) {
PointerBinding.instance.debugOverrideDetector(context);
// This can happen with the following gesture sequence:
//
// LMB: down-------------------up
// RMB: down------------------up
// Flutter: down-------move-------move-------up
List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
packets.add(packet);
};
// Press and hold LMB.
glassPane.dispatchEvent(context.mouseDown(
button: 0,
buttons: 1,
clientX: 5.0,
clientY: 100.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(2));
expect(packets[0].data[0].change, equals(ui.PointerChange.add));
expect(packets[0].data[0].synthesized, equals(true));
expect(packets[0].data[1].change, equals(ui.PointerChange.down));
expect(packets[0].data[1].synthesized, equals(false));
expect(packets[0].data[1].buttons, equals(1));
expect(packets[0].data[1].physicalX, equals(5.0 * dpi));
expect(packets[0].data[1].physicalY, equals(100.0 * dpi));
packets.clear();
// Press and hold RMB. The pointer is already down, so we only send a move
// to update the position of the pointer.
glassPane.dispatchEvent(context.mouseDown(
button: 2,
buttons: 3,
clientX: 20.0,
clientY: 100.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(1));
expect(packets[0].data[0].change, equals(ui.PointerChange.move));
expect(packets[0].data[0].buttons, equals(3));
expect(packets[0].data[0].physicalX, equals(20.0 * dpi));
expect(packets[0].data[0].physicalY, equals(100.0 * dpi));
packets.clear();
// Release LMB. The pointer is still down (RMB), so we only send a move to
// update the position of the pointer.
glassPane.dispatchEvent(context.mouseUp(
button: 0,
buttons: 2,
clientX: 30.0,
clientY: 100.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(1));
expect(packets[0].data[0].change, equals(ui.PointerChange.move));
expect(packets[0].data[0].buttons, equals(2));
expect(packets[0].data[0].physicalX, equals(30.0 * dpi));
expect(packets[0].data[0].physicalY, equals(100.0 * dpi));
packets.clear();
// Release RMB. There's no more buttons down, so we send an up event.
glassPane.dispatchEvent(context.mouseUp(
button: 2,
buttons: 0,
clientX: 30.0,
clientY: 100.0,
));
expect(packets, hasLength(1));
expect(packets[0].data, hasLength(1));
expect(packets[0].data[0].change, equals(ui.PointerChange.up));
expect(packets[0].data[0].buttons, equals(0));
packets.clear();
},
);
_testEach<_ButtonedEventMixin>(
[
if (!isIosSafari) _PointerEventContext(),
......@@ -2141,7 +2225,7 @@ mixin _ButtonedEventMixin on _BasicEventContext {
{double clientX, double clientY, int button, int buttons});
// Generate an event that releases all mouse buttons.
html.Event mouseUp({double clientX, double clientY, int button});
html.Event mouseUp({double clientX, double clientY, int button, int buttons});
html.Event hover({double clientX, double clientY}) {
return mouseMove(
......@@ -2180,6 +2264,27 @@ mixin _ButtonedEventMixin on _BasicEventContext {
clientY: clientY,
);
}
html.Event wheel({
@required int buttons,
@required double clientX,
@required double clientY,
@required double deltaX,
@required double deltaY,
}) {
final Function jsWheelEvent = js_util.getProperty(html.window, 'WheelEvent');
final List<dynamic> eventArgs = <dynamic>[
'wheel',
<String, dynamic>{
'buttons': buttons,
'clientX': clientX,
'clientY': clientY,
'deltaX': deltaX,
'deltaY': deltaY,
}
];
return js_util.callConstructor(jsWheelEvent, js_util.jsify(eventArgs));
}
}
class _TouchDetails {
......@@ -2362,10 +2467,10 @@ class _MouseEventContext extends _BasicEventContext
}
@override
html.Event mouseUp({double clientX, double clientY, int button}) {
html.Event mouseUp({double clientX, double clientY, int button, int buttons}) {
return _createMouseEvent(
'mouseup',
buttons: 0,
buttons: buttons,
button: button,
clientX: clientX,
clientY: clientY,
......@@ -2518,10 +2623,11 @@ class _PointerEventContext extends _BasicEventContext
}
@override
html.Event mouseUp({double clientX, double clientY, int button}) {
html.Event mouseUp({double clientX, double clientY, int button, int buttons}) {
return _upWithFullDetails(
pointer: 1,
button: button,
buttons: buttons,
clientX: clientX,
clientY: clientY,
pointerType: 'mouse',
......@@ -2532,12 +2638,13 @@ class _PointerEventContext extends _BasicEventContext
{double clientX,
double clientY,
int button,
int buttons,
int pointer,
String pointerType}) {
return html.PointerEvent('pointerup', <String, dynamic>{
'pointerId': pointer,
'button': button,
'buttons': 0,
'buttons': buttons,
'clientX': clientX,
'clientY': clientY,
'pointerType': pointerType,
......
......@@ -50,9 +50,7 @@ void testMain() async {
canvas.drawParagraph(paragraph, offset);
// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
fillPlaceholder(canvas, offset, paragraph);
offset = offset.translate(0.0, paragraph.height + 30.0);
}
......@@ -86,9 +84,7 @@ void testMain() async {
canvas.drawParagraph(paragraph, offset);
// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
fillPlaceholder(canvas, offset, paragraph);
offset = offset.translate(0.0, paragraph.height + 30.0);
}
......@@ -122,13 +118,89 @@ void testMain() async {
canvas.drawParagraph(paragraph, offset);
// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
fillPlaceholder(canvas, offset, paragraph);
offset = offset.translate(0.0, paragraph.height + 30.0);
}
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align_dom');
});
test('draws paragraphs starting or ending with a placeholder', () {
const Rect bounds = Rect.fromLTWH(0, 0, 420, 300);
final canvas = BitmapCanvas(bounds, RenderStrategy());
Offset offset = Offset(10, 10);
// First paragraph with a placeholder at the beginning.
final CanvasParagraph paragraph1 = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
(builder) {
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
builder.pushStyle(TextStyle(color: black));
builder.addText(' Lorem ipsum.');
},
)..layout(constrain(400.0));
// Draw the paragraph.
canvas.drawParagraph(paragraph1, offset);
fillPlaceholder(canvas, offset, paragraph1);
surroundParagraph(canvas, offset, paragraph1);
offset = offset.translate(0.0, paragraph1.height + 30.0);
// Second paragraph with a placeholder at the end.
final CanvasParagraph paragraph2 = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
(builder) {
builder.pushStyle(TextStyle(color: black));
builder.addText('Lorem ipsum ');
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
},
)..layout(constrain(400.0));
// Draw the paragraph.
canvas.drawParagraph(paragraph2, offset);
fillPlaceholder(canvas, offset, paragraph2);
surroundParagraph(canvas, offset, paragraph2);
offset = offset.translate(0.0, paragraph2.height + 30.0);
// Third paragraph with a placeholder alone in the second line.
final CanvasParagraph paragraph3 = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 24.0, textAlign: TextAlign.center),
(builder) {
builder.pushStyle(TextStyle(color: black));
builder.addText('Lorem ipsum ');
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
},
)..layout(constrain(200.0));
// Draw the paragraph.
canvas.drawParagraph(paragraph3, offset);
fillPlaceholder(canvas, offset, paragraph3);
surroundParagraph(canvas, offset, paragraph3);
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_start_and_end');
});
}
void surroundParagraph(
EngineCanvas canvas,
Offset offset,
CanvasParagraph paragraph,
) {
final Rect rect = offset & Size(paragraph.width, paragraph.height);
final SurfacePaint paint = Paint()..color = blue..style = PaintingStyle.stroke;
canvas.drawRect(rect, paint.paintData);
}
void fillPlaceholder(
EngineCanvas canvas,
Offset offset,
CanvasParagraph paragraph,
) {
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint paint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), paint.paintData);
}
......@@ -149,4 +149,64 @@ void testMain() async {
l('ipsum', 6, 11, hardBreak: true, width: 50.0, left: 0.0),
]);
});
test('should handle placeholder-only paragraphs', () {
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
fontFamily: 'ahem',
fontSize: 10,
textAlign: ui.TextAlign.center,
);
final CanvasParagraph paragraph = rich(paragraphStyle, (builder) {
builder.addPlaceholder(300.0, 50.0, ui.PlaceholderAlignment.baseline, baseline: ui.TextBaseline.alphabetic);
})..layout(constrain(500.0));
expect(paragraph.maxIntrinsicWidth, 300.0);
expect(paragraph.minIntrinsicWidth, 300.0);
expect(paragraph.height, 50.0);
expectLines(paragraph, [
l('', 0, 0, hardBreak: true, width: 300.0, left: 100.0),
]);
});
test('correct maxIntrinsicWidth when paragraph ends with placeholder', () {
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
fontFamily: 'ahem',
fontSize: 10,
textAlign: ui.TextAlign.center,
);
final CanvasParagraph paragraph = rich(paragraphStyle, (builder) {
builder.addText('abcd');
builder.addPlaceholder(300.0, 50.0, ui.PlaceholderAlignment.bottom);
})..layout(constrain(400.0));
expect(paragraph.maxIntrinsicWidth, 340.0);
expect(paragraph.minIntrinsicWidth, 300.0);
expect(paragraph.height, 50.0);
expectLines(paragraph, [
l('abcd', 0, 4, hardBreak: true, width: 340.0, left: 30.0),
]);
});
test('handles new line followed by a placeholder', () {
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
fontFamily: 'ahem',
fontSize: 10,
textAlign: ui.TextAlign.center,
);
final CanvasParagraph paragraph = rich(paragraphStyle, (builder) {
builder.addText('Lorem\n');
builder.addPlaceholder(300.0, 40.0, ui.PlaceholderAlignment.bottom);
builder.addText('ipsum');
})..layout(constrain(300.0));
// The placeholder's width + "ipsum"
expect(paragraph.maxIntrinsicWidth, 300.0 + 50.0);
expect(paragraph.minIntrinsicWidth, 300.0);
expect(paragraph.height, 10.0 + 40.0 + 10.0);
expectLines(paragraph, [
l('Lorem', 0, 6, hardBreak: true, width: 50.0, height: 10.0, left: 125.0),
l('', 6, 6, hardBreak: false, width: 300.0, height: 40.0, left: 0.0),
l('ipsum', 6, 11, hardBreak: true, width: 50.0, height: 10.0, left: 125.0),
]);
});
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册