diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 33e7d061a808ef90cd5f84776ca11b8ec52e8514..b13434a53e5b72e41ee8c607621688c8f5cb216f 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: ec80c8042759905a5215ab1cd87ad280e8ef3cd7 +revision: 4b4c256d6124a135b70c1a9a7ff10cf2827df31c diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 47c7092e515f8bdfe00965b2c5276605f53bb1b4..c8247c39894fda4c4b96cc4d8ba8ed945295668f 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -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 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 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 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 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 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 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 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 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 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({ diff --git a/lib/web_ui/lib/src/engine/pointer_converter.dart b/lib/web_ui/lib/src/engine/pointer_converter.dart index 26b72a08f9b85488f57ba68b5047aae2d7be927b..c2370d8ea187ebb4b6007cd06180ea616cf522d6 100644 --- a/lib/web_ui/lib/src/engine/pointer_converter.dart +++ b/lib/web_ui/lib/src/engine/pointer_converter.dart @@ -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, diff --git a/lib/web_ui/lib/src/engine/text/layout_service.dart b/lib/web_ui/lib/src/engine/text/layout_service.dart index 57cf525004f711d2b7db3973ea382ca42a83f5a0..f264daf91232560a25ecadfa59f201d3d07d2880 100644 --- a/lib/web_ui/lib/src/engine/text/layout_service.dart +++ b/lib/web_ui/lib/src/engine/text/layout_service.dart @@ -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; diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 693f43cc349946a6d9f4bce2ea9b2798c848f7b5..8aa7efd4f2edc6cdd8b63fda8a76f2d152d67ae7 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -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 packets = []; + 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 eventArgs = [ + 'wheel', + { + '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', { 'pointerId': pointer, 'button': button, - 'buttons': 0, + 'buttons': buttons, 'clientX': clientX, 'clientY': clientY, 'pointerType': pointerType, diff --git a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart index 45440b03dc3a98a658967385bc2b7887ab8628e6..a1764aa33ddd82c935c0dda6aa9b96b3960e4c69 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart @@ -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); } diff --git a/lib/web_ui/test/text/layout_service_rich_test.dart b/lib/web_ui/test/text/layout_service_rich_test.dart index d6b9df01e9bb924558e6b1396660292cc5d05913..8ed16095a26923d59d8ba186833834f984ae8568 100644 --- a/lib/web_ui/test/text/layout_service_rich_test.dart +++ b/lib/web_ui/test/text/layout_service_rich_test.dart @@ -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), + ]); + }); }