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

[web] Add dynamic view sizing for non-implicit views. #48541

Closed
wants to merge 10 commits into from
Closed
2 changes: 0 additions & 2 deletions lib/web_ui/lib/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ abstract class PlatformDispatcher {

void scheduleFrame();

void render(Scene scene, [FlutterView view]);

AccessibilityFeatures get accessibilityFeatures;

VoidCallback? get onAccessibilityFeaturesChanged;
Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class Rasterizer {

/// Creates a new frame from this rasterizer's surface, draws the given
/// [LayerTree] into it, and then submits the frame.
void draw(LayerTree layerTree) {
final ui.Size frameSize = view.physicalSize;
void draw(LayerTree layerTree, { ui.Size? size }) {
final ui.Size frameSize = size ?? view.physicalSize;
if (frameSize.isEmpty) {
// Available drawing area is empty. Skip drawing.
return;
Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ class CanvasKitRenderer implements Renderer {
CkParagraphBuilder(style);

@override
void renderScene(ui.Scene scene, ui.FlutterView view) {
void renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) {
// "Build finish" and "raster start" happen back-to-back because we
// render on the same thread, so there's no overhead from hopping to
// another thread.
Expand All @@ -417,7 +417,7 @@ class CanvasKitRenderer implements Renderer {
"Unable to render to a view which hasn't been registered");
final Rasterizer rasterizer = _rasterizers[view.viewId]!;

rasterizer.draw((scene as LayerScene).layerTree);
rasterizer.draw((scene as LayerScene).layerTree, size: size);
Copy link
Member Author

Choose a reason for hiding this comment

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

@harryterkelsen is there a way of testing this? Somehow asserting that the thing that was drawn matches the size of the incoming size?

Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC the size is only used to size the output canvases before drawing to them, so if you wanted to test this then I think you should call draw with an explicit size and then check that the bitmap created and the bitmaprenderer canvases are the same size as the size that is passed in.

frameTimingsOnRasterFinish();
}

Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/html/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ class HtmlRenderer implements Renderer {
CanvasParagraphBuilder(style as EngineParagraphStyle);

@override
void renderScene(ui.Scene scene, ui.FlutterView view) {
void renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) {
final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!;
implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!);
frameTimingsOnRasterFinish();
Expand Down
20 changes: 20 additions & 0 deletions lib/web_ui/lib/src/engine/js_interop/js_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,31 @@ extension JsFlutterViewOptionsExtension on JsFlutterViewOptions {
return _hostElement!;
}

@JS('viewConstraints')
external JsViewConstraints? get _viewConstraints;
JsViewConstraints? get viewConstraints {
// Assert constraints are valid?
return _viewConstraints;
}

@JS('initialData')
external JSObject? get _initialData;
Object? get initialData => _initialData?.toObjectDeep;
}

/// The JS bindings for a [ViewConstraints] object.
@JS()
@staticInterop
class JsViewConstraints {}

/// The attributes of a [JsViewConstraints] object.
extension JsViewConstraintsExtension on JsViewConstraints {
external double? get maxHeight;
external double? get maxWidth;
external double? get minHeight;
external double? get minWidth;
}

/// The public JS API of a running Flutter Web App.
@JS()
@anonymous
Expand Down
20 changes: 12 additions & 8 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -727,8 +727,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
scheduleFrameCallback!();
}

/// Updates the application's rendering on the GPU with the newly provided
/// [Scene]. This function must be called within the scope of the
/// Updates the [view]'s rendering on the GPU with the newly provided [scene] of physical [size].
///
/// This function must be called within the scope of the
/// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function
/// is called a second time during a single [onBeginFrame]/[onDrawFrame]
/// callback sequence or called outside the scope of those callbacks, the call
Expand All @@ -751,15 +752,18 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
/// scheduling of frames.
/// * [RendererBinding], the Flutter framework class which manages layout and
/// painting.
@override
void render(ui.Scene scene, [ui.FlutterView? view]) {
assert(view != null || implicitView != null,
'Calling render without a FlutterView');
if (view == null && implicitView == null) {
void render(ui.Scene scene, { ui.FlutterView? view, ui.Size? size }) {
final ui.FlutterView? target = view ?? implicitView;
assert(target != null, 'Calling render without a FlutterView');
if (target == null) {
// If there is no view to render into, then this is a no-op.
return;
}
renderer.renderScene(scene, view ?? implicitView!);

if (size != null && view is EngineFlutterView) {
view.dom.resize(size / view.devicePixelRatio);
}
renderer.renderScene(scene, target, size: size);
}

/// Additional accessibility features that may be enabled by the platform.
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -222,5 +222,5 @@ abstract class Renderer {

ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style);

FutureOr<void> renderScene(ui.Scene scene, ui.FlutterView view);
FutureOr<void> renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size });
}
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ class SkwasmRenderer implements Renderer {
// TODO(harryterkelsen): Add multiview support,
// https://github.com/flutter/flutter/issues/137073.
@override
Future<void> renderScene(ui.Scene scene, ui.FlutterView view) =>
Future<void> renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) =>
sceneView.renderScene(scene as EngineScene);

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class SkwasmRenderer implements Renderer {
}

@override
void renderScene(ui.Scene scene, ui.FlutterView view) {
void renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) {
throw UnimplementedError('Skwasm not implemented on this platform.');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ class CustomElementDimensionsProvider extends DimensionsProvider {
final StreamController<ui.Size> _onResizeStreamController =
StreamController<ui.Size>.broadcast();

// A cache of the last Size reported by the browser for hostElement, so this
// never has to hit the clientWidth/Height metrics from the DOM.
ui.Size? _lastObservedSize;

// Broadcasts the last seen `Size`.
void _broadcastSize(ui.Size size) {
_lastObservedSize = size;
_onResizeStreamController.add(size);
}

Expand All @@ -68,10 +73,10 @@ class CustomElementDimensionsProvider extends DimensionsProvider {
ui.Size computePhysicalSize() {
final double devicePixelRatio = getDevicePixelRatio();

return ui.Size(
_hostElement.clientWidth * devicePixelRatio,
_hostElement.clientHeight * devicePixelRatio,
);
final ui.Size size = _lastObservedSize ??
ui.Size(_hostElement.clientWidth, _hostElement.clientHeight);

return size * devicePixelRatio;
}

@override
Expand Down
7 changes: 7 additions & 0 deletions lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ class DomManager {
sceneHost.append(sceneElement);
}
}

/// Resizes the [rootElement] to [logicalSize] (in px) via CSS.
void resize(ui.Size logicalSize) {
rootElement.style
..width = '${logicalSize.width}px'
..height = '${logicalSize.height}px';
}
}

DomShadowRoot _attachShadowRoot(DomElement element) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ class FlutterViewManager {
EngineFlutterView createAndRegisterView(
JsFlutterViewOptions jsViewOptions,
) {
final EngineFlutterView view =
EngineFlutterView(_dispatcher, jsViewOptions.hostElement);
final EngineFlutterView view = EngineFlutterView(
_dispatcher,
jsViewOptions.hostElement,
viewConstraints: jsViewOptions.viewConstraints,
);
registerView(view, jsViewOptions: jsViewOptions);
return view;
}
Expand Down
91 changes: 73 additions & 18 deletions lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../engine.dart' show DimensionsProvider, registerHotRestartListener, ren
import 'browser_detection.dart';
import 'display.dart';
import 'dom.dart';
import 'js_interop/js_app.dart' show JsViewConstraints, JsViewConstraintsExtension;
import 'mouse/context_menu.dart';
import 'mouse/cursor.dart';
import 'navigation/history.dart';
Expand Down Expand Up @@ -48,17 +49,26 @@ base class EngineFlutterView implements ui.FlutterView {
/// the Flutter view will be rendered.
factory EngineFlutterView(
EnginePlatformDispatcher platformDispatcher,
DomElement hostElement,
DomElement hostElement, {
JsViewConstraints? viewConstraints,
}
) = _EngineFlutterViewImpl;

EngineFlutterView._(
this.viewId,
this.platformDispatcher,
// This is nullable to accommodate the legacy `EngineFlutterWindow`. In
// multi-view mode, the host element is required for each view (as reflected
// by the public `EngineFlutterView` constructor).
DomElement? hostElement,
) : embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement),
{
// This is configurable for the implicit view that sets it to a specific
// kImplicitViewId value. Otherwise, this could be auto-incremental and
// not configurable.
required this.viewId,
// This is nullable to accommodate the legacy `EngineFlutterWindow`. In
// multi-view mode, the host element is required for each view (as reflected
// by the public `EngineFlutterView` constructor).
DomElement? hostElement,
JsViewConstraints? viewConstraints,
}
) : _jsViewConstraints = viewConstraints,
embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement),
dimensionsProvider = DimensionsProvider.create(hostElement: hostElement) {
// The embeddingStrategy will take care of cleaning up the rootElement on
// hot restart.
Expand Down Expand Up @@ -107,10 +117,9 @@ base class EngineFlutterView implements ui.FlutterView {
}

@override
void render(ui.Scene scene, {ui.Size? size}) {
void render(ui.Scene scene, { ui.Size? size }) {
assert(!isDisposed, 'Trying to render a disposed EngineFlutterView.');
// TODO(goderbauer): Respect the provided size when "physicalConstraints" are not always tight. See TODO on "physicalConstraints".
platformDispatcher.render(scene, this);
platformDispatcher.render(scene, view: this, size: size);
}

@override
Expand All @@ -135,9 +144,13 @@ base class EngineFlutterView implements ui.FlutterView {

late final PointerBinding pointerBinding;

// TODO(goderbauer): Provide API to configure constraints. See also TODO in "render".
@override
ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize);
ViewConstraints get physicalConstraints {
_computePhysicalSize();
return ViewConstraints.fromJsOptions(_jsViewConstraints, physicalSize);
}
// The configured constraints used to compute the actual physicalConstraints.
final JsViewConstraints? _jsViewConstraints;

late final EngineSemanticsOwner semantics = EngineSemanticsOwner(dom.semanticsHost);

Expand Down Expand Up @@ -267,17 +280,19 @@ base class EngineFlutterView implements ui.FlutterView {

final class _EngineFlutterViewImpl extends EngineFlutterView {
_EngineFlutterViewImpl(
EnginePlatformDispatcher platformDispatcher,
DomElement hostElement,
) : super._(_nextViewId++, platformDispatcher, hostElement);
super.platformDispatcher,
DomElement hostElement, {
super.viewConstraints,
}
) : super._(viewId: _nextViewId++, hostElement: hostElement);
}

/// The Web implementation of [ui.SingletonFlutterWindow].
final class EngineFlutterWindow extends EngineFlutterView implements ui.SingletonFlutterWindow {
EngineFlutterWindow._(
EnginePlatformDispatcher platformDispatcher,
super.platformDispatcher,
DomElement? hostElement,
) : super._(kImplicitViewId, platformDispatcher, hostElement) {
) : super._(viewId: kImplicitViewId, hostElement: hostElement) {
if (ui_web.isCustomUrlStrategySet) {
_browserHistory = createHistoryForExistingState(ui_web.urlStrategy);
}
Expand Down Expand Up @@ -643,7 +658,7 @@ final class EngineFlutterWindow extends EngineFlutterView implements ui.Singleto
EngineFlutterWindow get window {
assert(
_window != null,
'Trying to access the implicit FlutterView, but it is not available.\n'
'Trying to access the implicit FlutterView, but it is not available.'
'Note: the implicit FlutterView is not available in multi-view mode.',
);
return _window!;
Expand Down Expand Up @@ -692,12 +707,52 @@ class ViewConstraints implements ui.ViewConstraints {
this.maxHeight = double.infinity,
});

factory ViewConstraints.fromJsOptions(JsViewConstraints? constraints, ui.Size? size) {
if (size == null) {
return const ViewConstraints();
}
if (constraints == null) {
return ViewConstraints.tight(size);
}
return ViewConstraints(
minWidth: _computeMinValue(constraints.minWidth, size.width),
minHeight: _computeMinValue(constraints.minHeight, size.height),
maxWidth: _computeMaxValue(constraints.maxWidth, size.width),
maxHeight: _computeMaxValue(constraints.maxHeight, size.height),
);
}

ViewConstraints.tight(ui.Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;

// Computes the "min" value for a constraint that takes into account user configuration
// and the actual available size.
//
// Returns the configured userValue, unless it's null (not passed) in which it returns
// the actual physicalSize.
static double _computeMinValue(double? userValue, double physicalSize) {
assert(userValue == null || userValue >= 0, 'Minimum constraint cannot be less than 0');
return userValue ?? physicalSize;
}

// Computes the "max" value for a constraint that takes into account user configuration
// and the available size.
//
// Returns the configured userValue unless:
// * It is null, in which case it returns the physicalSize
// * It is `-1`, in which case it returns "infinity" / unconstrained.
static double _computeMaxValue(double? userValue, double physicalSize) {
assert(userValue == null || userValue >= -1, 'Maximum constraint must be greater than 0 (or -1 for unconstrained)');
return switch (userValue) {
null => physicalSize,
-1 => double.infinity,
_ => userValue,
};
}

@override
final double minWidth;
@override
Expand Down
Loading