Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] De-singletonize MouseCursor for multi-view #45295

Merged
merged 9 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -2038,7 +2038,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font_encoding.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -4787,7 +4787,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font_encoding.dart
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export 'engine/js_interop/js_typed_data.dart';
export 'engine/key_map.g.dart';
export 'engine/keyboard_binding.dart';
export 'engine/layers.dart';
export 'engine/mouse_cursor.dart';
export 'engine/mouse/cursor.dart';
export 'engine/navigation/history.dart';
export 'engine/noto_font.dart';
export 'engine/noto_font_encoding.dart';
Expand Down
2 changes: 2 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration {
set alignContent(String value) => setProperty('align-content', value);
set textAlign(String value) => setProperty('text-align', value);
set font(String value) => setProperty('font', value);
set cursor(String value) => setProperty('cursor', value);
String get width => getPropertyValue('width');
String get height => getPropertyValue('height');
String get position => getPropertyValue('position');
Expand Down Expand Up @@ -807,6 +808,7 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration {
String get alignContent => getPropertyValue('align-content');
String get textAlign => getPropertyValue('text-align');
String get font => getPropertyValue('font');
String get cursor => getPropertyValue('cursor');

@JS('getPropertyValue')
external JSString _getPropertyValue(JSString property);
Expand Down
1 change: 0 additions & 1 deletion lib/web_ui/lib/src/engine/initialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ Future<void> initializeEngineUi() async {
_initializationState = DebugEngineInitializationState.initializingUi;

RawKeyboard.initialize(onMacOs: operatingSystem == OperatingSystem.macOs);
MouseCursor.initialize();
ensureFlutterViewEmbedderInitialized();
_initializationState = DebugEngineInitializationState.initialized;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'embedder.dart';
import 'util.dart';
import '../dom.dart';

/// Provides mouse cursor bindings, such as the `flutter/mousecursor` channel.
/// Controls the mouse cursor in the given [element].
class MouseCursor {
MouseCursor._();
MouseCursor(this.element);

/// Initializes the [MouseCursor] singleton.
///
/// Use the [instance] getter to get the singleton after calling this method.
static void initialize() {
_instance ??= MouseCursor._();
}

/// The [MouseCursor] singleton.
static MouseCursor? get instance => _instance;
static MouseCursor? _instance;
final DomElement element;

// Map from Flutter's kind values to CSS's cursor values.
//
Expand Down Expand Up @@ -61,15 +51,12 @@ class MouseCursor {
'zoomIn': 'zoom-in',
'zoomOut': 'zoom-out',
};

static String _mapKindToCssValue(String? kind) {
return _kindToCssValueMap[kind] ?? 'default';
}

void activateSystemCursor(String? kind) {
setElementStyle(
flutterViewEmbedder.flutterViewElement,
'cursor',
_mapKindToCssValue(kind),
);
element.style.cursor = _mapKindToCssValue(kind);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only problem I can imagine with this is that if the element gets detached/deleted from the DOM, and then a system cursor event message comes, this might attempt to operate on a null instance. Since this is speculation and not a bug that we've seen so far, I say we don't worry about it for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not a problem at all! The element is non-nullable and cannot become null. Yes, it can be detached/removed from the DOM tree, but it is still a valid element. In fact, in cursor_test.dart, we are using detached elements to test MouseCursor.

}
}
16 changes: 5 additions & 11 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
/// * [PlatformDisptacher.views] for a list of all [FlutterView]s provided
/// by the platform.
@override
ui.FlutterView? get implicitView => viewData[kImplicitViewId];
EngineFlutterWindow? get implicitView => viewData[kImplicitViewId] as EngineFlutterWindow?;

/// A callback that is invoked whenever the platform's [devicePixelRatio],
/// [physicalSize], [padding], [viewInsets], or [systemGestureInsets]
Expand Down Expand Up @@ -505,10 +505,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
// TODO(a-wallen): As multi-window support expands, the pop call
// will need to include the view ID. Right now only one view is
// supported.
(viewData[kImplicitViewId]! as EngineFlutterWindow)
.browserHistory
.exit()
.then((_) {
implicitView!.browserHistory.exit().then((_) {
replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true));
});
Expand Down Expand Up @@ -585,7 +582,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
final Map<dynamic, dynamic> arguments = decoded.arguments as Map<dynamic, dynamic>;
switch (decoded.method) {
case 'activateSystemCursor':
MouseCursor.instance!.activateSystemCursor(arguments.tryString('kind'));
implicitView!.mouseCursor.activateSystemCursor(arguments.tryString('kind'));
}
return;

Expand Down Expand Up @@ -618,9 +615,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
// TODO(a-wallen): As multi-window support expands, the navigation call
// will need to include the view ID. Right now only one view is
// supported.
(viewData[kImplicitViewId]! as EngineFlutterWindow)
.handleNavigationMessage(data)
.then((bool handled) {
implicitView!.handleNavigationMessage(data).then((bool handled) {
if (handled) {
const MethodCodec codec = JSONMethodCodec();
replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
Expand Down Expand Up @@ -1231,8 +1226,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
/// requests from the embedder.
@override
String get defaultRouteName {
return _defaultRouteName ??=
(viewData[kImplicitViewId]! as EngineFlutterWindow).browserHistory.currentPath;
return _defaultRouteName ??= implicitView!.browserHistory.currentPath;
}

/// Lazily initialized when the `defaultRouteName` getter is invoked.
Expand Down
19 changes: 18 additions & 1 deletion lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../engine.dart' show DimensionsProvider, registerHotRestartListener, renderer;
import 'display.dart';
import 'dom.dart';
import 'embedder.dart';
import 'mouse/cursor.dart';
import 'navigation/history.dart';
import 'platform_dispatcher.dart';
import 'services.dart';
Expand All @@ -29,8 +31,17 @@ const bool debugPrintPlatformMessages = false;
/// The view ID for the implicit flutter view provided by the platform.
const int kImplicitViewId = 0;

/// Represents all views in the Flutter Web Engine.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the class that represents all views in the Flutter Web Engine is EngineFlutterWindow (that's the type that was used to define the implicitView, for example), right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EngineFlutterWindow is a misnomer. It should be called EngineSingletonFlutterWindow (it actually extends ui.SingletonFlutterWindow). Views in the multi-view world shouldn't inherit from this singleton class. They should inherit directly from ui.FlutterView (and implement EngineFlutterView to satisfy all the web-specific needs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a follow up PR to rename this class to make it clear that it's a singleton: #45981

///
/// In addition to everything defined in [ui.FlutterView], this class adds
/// a few web-specific properties.
abstract interface class EngineFlutterView extends ui.FlutterView {
MouseCursor get mouseCursor;
DomElement get rootElement;
}

/// The Web implementation of [ui.SingletonFlutterWindow].
class EngineFlutterWindow extends ui.SingletonFlutterWindow {
class EngineFlutterWindow extends ui.SingletonFlutterWindow implements EngineFlutterView {
EngineFlutterWindow(this.viewId, this.platformDispatcher) {
platformDispatcher.viewData[viewId] = this;
platformDispatcher.windowConfigurations[viewId] = const ViewConfiguration();
Expand All @@ -53,6 +64,12 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
@override
final EnginePlatformDispatcher platformDispatcher;

@override
late final MouseCursor mouseCursor = MouseCursor(rootElement);

@override
DomElement get rootElement => flutterViewEmbedder.flutterViewElement;

/// Handles the browser history integration to allow users to use the back
/// button, etc.
BrowserHistory get browserHistory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ void testMain() {

expect(findGlassPane(), isNull);
expect(RawKeyboard.instance, isNull);
expect(MouseCursor.instance, isNull);
expect(KeyboardBinding.instance, isNull);
expect(PointerBinding.instance, isNull);

Expand All @@ -28,15 +27,13 @@ void testMain() {

expect(findGlassPane(), isNull);
expect(RawKeyboard.instance, isNull);
expect(MouseCursor.instance, isNull);
expect(KeyboardBinding.instance, isNull);
expect(PointerBinding.instance, isNull);

// Now UI should be taken over by Flutter.
await initializeEngineUi();
expect(findGlassPane(), isNotNull);
expect(RawKeyboard.instance, isNotNull);
expect(MouseCursor.instance, isNotNull);
expect(KeyboardBinding.instance, isNotNull);
expect(PointerBinding.instance, isNotNull);
});
Expand Down
40 changes: 40 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,40 @@
// 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';

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

void testMain() {
group('$MouseCursor', () {
test('sets correct `cursor` style on root element', () {
final DomElement rootViewElement = createDomElement('div');
final MouseCursor mouseCursor = MouseCursor(rootViewElement);

mouseCursor.activateSystemCursor('alias');
expect(rootViewElement.style.cursor, 'alias');

mouseCursor.activateSystemCursor('move');
expect(rootViewElement.style.cursor, 'move');

mouseCursor.activateSystemCursor('precise');
expect(rootViewElement.style.cursor, 'crosshair');

mouseCursor.activateSystemCursor('resizeDownRight');
expect(rootViewElement.style.cursor, 'se-resize');
});

test('handles unknown cursor type', () {
final DomElement rootViewElement = createDomElement('div');
final MouseCursor mouseCursor = MouseCursor(rootViewElement);

mouseCursor.activateSystemCursor('unknown');
expect(rootViewElement.style.cursor, 'default');
});
});
}