Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

feat: add custom cursor capabilities to web platform. Addresses #31952, #89351 and #99484 #41186

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c0598c
change _mapKindToCssValueIO in mouse_cursor.dart to allow css image-s…
timmaffett Apr 13, 2023
161fe9e
Merge branch 'web_custom_cursors' of https://github.com/timmaffett/en…
timmaffett Apr 13, 2023
624b082
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 14, 2023
1ae28cb
remove extra parens
timmaffett Apr 14, 2023
44cedf1
Merge branch 'main' into web_custom_cursors
timmaffett Apr 14, 2023
495eb0e
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 14, 2023
587ba97
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 18, 2023
c11501d
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 18, 2023
d2eb519
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 20, 2023
f065ab5
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 21, 2023
f08ea30
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 23, 2023
f733932
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 24, 2023
ad53f4f
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 25, 2023
0f86a4e
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 27, 2023
240f1b3
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 27, 2023
22ba5d2
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 28, 2023
4d7d15b
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 28, 2023
b6ebb70
Merge branch 'flutter:main' into web_custom_cursors
timmaffett Apr 29, 2023
a778807
add tests for mouse_cursor.dart changes, fix typo in readme
timmaffett Apr 30, 2023
f36cc10
add name to AUTHORS
timmaffett Apr 30, 2023
e104339
Merge branch 'flutter:main' into web_custom_cursors
timmaffett May 20, 2023
a16a910
Merge branch 'web_custom_cursors' of https://github.com/timmaffett/en…
timmaffett Jun 1, 2023
d28bfc5
make tests more flexible to detect image-set or -webkit-image-set in …
timmaffett Jun 2, 2023
66cca69
fix whitespace error
timmaffett Jun 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ Koutaro Mori <[email protected]>
TheOneWithTheBraid <[email protected]>
Twin Sun, LLC <[email protected]>
Qixing Cao <[email protected]>
LinXunFeng <[email protected]>
LinXunFeng <[email protected]>
Tim Maffett <[email protected]>
2 changes: 1 addition & 1 deletion lib/web_ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The `build` subcommand builds web engine gn/ninja targets. Targets can be
individually specified in the command line invocation, or if none are specified,
all web engine targets are built. Common targets are as follows:
* `sdk` - The flutter_web_sdk itself.
* `canvaskit` - Flutter's version of canvakit.
* `canvaskit` - Flutter's version of canvaskit.
* `canvaskit_chromium` - A version of canvaskit optimized for use with
chromium-based browsers.
* `skwasm` - Builds experimental skia wasm module renderer.
Expand Down
14 changes: 13 additions & 1 deletion lib/web_ui/lib/src/engine/mouse_cursor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,19 @@ class MouseCursor {
'zoomOut': 'zoom-out',
};
static String _mapKindToCssValue(String? kind) {
return _kindToCssValueMap[kind] ?? 'default';
// Allow 'image-set(...)'/'-webkit-image-set(...)' css commands thru for setting DevicePixelRatio
// aware images for cursors (for newer browsers) and allow 'url(...)' strings thru as fallback
// for older browsers (bare url() commands are always 1.0x dpr). The hotspot coordinates are always
// supplied in 1.0x dpr coordinates. All of these css methods can use data-uri versions of url's
// which allow for inline definition of image data to define the cursor.
if (kind != null &&
(kind.startsWith('url') ||
kind.startsWith('image-set') ||
kind.startsWith('-webkit-image-set'))) {
return kind;
} else {
return _kindToCssValueMap[kind] ?? 'default';
}
}

void activateSystemCursor(String? kind) {
Expand Down
124 changes: 124 additions & 0 deletions lib/web_ui/test/engine/mouse_cursor_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2013 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';

const String flutterLogoPngAsDataUri = 'data:image/png;base64,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';

// Following [_kindToCssValueMap] array definition is from flutter engine /lib/web_ui/lib/src/engine/mouse_cursor.dart
//
// Map from Flutter's kind values to CSS's cursor values.
//
// This map must be kept in sync with Flutter framework's
// rendering/mouse_cursor.dart.
const Map<String, String> _kindToCssValueMap = <String, String>{
'alias': 'alias',
'allScroll': 'all-scroll',
'basic': 'default',
'cell': 'cell',
'click': 'pointer',
'contextMenu': 'context-menu',
'copy': 'copy',
'forbidden': 'not-allowed',
'grab': 'grab',
'grabbing': 'grabbing',
'help': 'help',
'move': 'move',
'none': 'none',
'noDrop': 'no-drop',
'precise': 'crosshair',
'progress': 'progress',
'text': 'text',
'resizeColumn': 'col-resize',
'resizeDown': 's-resize',
'resizeDownLeft': 'sw-resize',
'resizeDownRight': 'se-resize',
'resizeLeft': 'w-resize',
'resizeLeftRight': 'ew-resize',
'resizeRight': 'e-resize',
'resizeRow': 'row-resize',
'resizeUp': 'n-resize',
'resizeUpDown': 'ns-resize',
'resizeUpLeft': 'nw-resize',
'resizeUpRight': 'ne-resize',
'resizeUpLeftDownRight': 'nwse-resize',
'resizeUpRightDownLeft': 'nesw-resize',
'verticalText': 'vertical-text',
'wait': 'wait',
'zoomIn': 'zoom-in',
'zoomOut': 'zoom-out',
};

void main() {
internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
ensureFlutterViewEmbedderInitialized();
final DomElement flutterViewElement = flutterViewEmbedder.flutterViewElement;

setUp(() {
});

MouseCursor.initialize();
final MouseCursor? mouseCursorTry = MouseCursor.instance;

assert(mouseCursorTry!=null,'Error MouseCursor.instance is null');

final MouseCursor mouseCursorInstance = mouseCursorTry!;

void setCursorKind(String kindToTest) {
mouseCursorInstance.activateSystemCursor( kindToTest );
}

String getSetCursorCSSStyle() {
final DomCSSStyleDeclaration style = domWindow.getComputedStyle(flutterViewElement);
return style.getPropertyValue('cursor');
}

// Make sure that all 'built in' MouseCursor kinds ([_kindToCssValueMap]) make it through to there css style mapping
test('MouseCursor.activateSystemCursor test built in system cursor to browser css map', () {
_kindToCssValueMap.forEach( (String key, String expectedResult) {
setCursorKind(key);
expect( getSetCursorCSSStyle(), equals(expectedResult) );
}
);
});

// We need to test that cursor key/kind values that start with
// 'url(..)', 'image-set(...)'/'-webkit-image-set(...)' css commands make it through.
test('MouseCursor.activateSystemCursor allows url/-webkit-image-set kind value prefix to pass', () {
const String urlCss = 'url("cursor.png")';
const String webkitImageSetCss = '-webkit-image-set($urlCss 1x) 0 0, $urlCss, help';

setCursorKind('grabbing'); // reset to different cursor before testing 'url...'

setCursorKind('$urlCss 4 12, default');
expect( getSetCursorCSSStyle(), startsWith('url') );

setCursorKind('progress'); // reset to different cursor before testing '-webkit-image-set...'

// we do test for '-webkit-image-set' or 'image-set' prefix, browsers may change '-webkit-image-set' to 'image-set'.
setCursorKind(webkitImageSetCss);
expect( getSetCursorCSSStyle(), anyOf(startsWith('-webkit-image-set'),startsWith('image-set')) );
});

test('MouseCursor.activateSystemCursor allows url/-webkit-image-set (with datauri url) kind value prefix to pass', () {
const String urlCss = 'url("$flutterLogoPngAsDataUri")';
const String webkitImageSetCss = '-webkit-image-set($urlCss 1x) 0 0, $urlCss, help';

setCursorKind('move'); // reset to different cursor before testing 'url...'

setCursorKind('$urlCss 4 12, default');
expect( getSetCursorCSSStyle(), startsWith('url') );

setCursorKind('progress'); // reset to different cursor before testing '-webkit-image-set...'

// we do test for '-webkit-image-set' or 'image-set' prefix, browsers may change '-webkit-image-set' to 'image-set'.
setCursorKind(webkitImageSetCss);
expect( getSetCursorCSSStyle(), anyOf(startsWith('-webkit-image-set'),startsWith('image-set')) );
});
}