| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/rendering.dart'; |
| 6 | library; |
| 7 | |
| 8 | import 'dart:ui_web' as ui_web; |
| 9 | |
| 10 | import 'package:flutter/foundation.dart'; |
| 11 | |
| 12 | import '../painting/_web_image_info_web.dart'; |
| 13 | import '../rendering/box.dart'; |
| 14 | import '../rendering/platform_view.dart'; |
| 15 | import '../rendering/shifted_box.dart'; |
| 16 | import '../web.dart' as web; |
| 17 | import 'basic.dart'; |
| 18 | import 'framework.dart'; |
| 19 | import 'platform_view.dart'; |
| 20 | |
| 21 | /// Displays an `<img>` element with `src` set to [src]. |
| 22 | class ImgElementPlatformView extends StatelessWidget { |
| 23 | /// Creates a platform view backed with an `<img>` element. |
| 24 | ImgElementPlatformView(this.src, {super.key}) { |
| 25 | if (!_registered) { |
| 26 | _register(); |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | static const String _viewType = 'Flutter__ImgElementImage__' ; |
| 31 | static bool _registered = false; |
| 32 | |
| 33 | static void _register() { |
| 34 | assert(!_registered); |
| 35 | _registered = true; |
| 36 | ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId, {Object? params}) { |
| 37 | final Map<Object?, Object?> paramsMap = params! as Map<Object?, Object?>; |
| 38 | // Create a new element. The browser is able to display the image |
| 39 | // without fetching it over the network again. |
| 40 | final web.HTMLImageElement img = web.document.createElement('img' ) as web.HTMLImageElement; |
| 41 | img.src = paramsMap['src' ]! as String; |
| 42 | // Set `width` and `height`, otherwise the engine will issue a warning. |
| 43 | img.style |
| 44 | ..width = '100%' |
| 45 | ..height = '100%' ; |
| 46 | return img; |
| 47 | }); |
| 48 | } |
| 49 | |
| 50 | /// The `src` URL for the `<img>` tag. |
| 51 | final String? src; |
| 52 | |
| 53 | @override |
| 54 | Widget build(BuildContext context) { |
| 55 | if (src == null) { |
| 56 | return const SizedBox.expand(); |
| 57 | } |
| 58 | return HtmlElementView( |
| 59 | viewType: _viewType, |
| 60 | creationParams: <String, String?>{'src' : src}, |
| 61 | hitTestBehavior: PlatformViewHitTestBehavior.transparent, |
| 62 | ); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /// A widget which displays and lays out an underlying HTML element in a |
| 67 | /// platform view. |
| 68 | class RawWebImage extends SingleChildRenderObjectWidget { |
| 69 | /// Creates a [RawWebImage]. |
| 70 | RawWebImage({ |
| 71 | super.key, |
| 72 | required this.image, |
| 73 | this.debugImageLabel, |
| 74 | this.width, |
| 75 | this.height, |
| 76 | this.fit, |
| 77 | this.alignment = Alignment.center, |
| 78 | this.matchTextDirection = false, |
| 79 | }) : super(child: ImgElementPlatformView(image.htmlImage.src)); |
| 80 | |
| 81 | /// The underlying HTML element to be displayed. |
| 82 | final WebImageInfo image; |
| 83 | |
| 84 | /// A debug label explaining the image. |
| 85 | final String? debugImageLabel; |
| 86 | |
| 87 | /// The requested width for this widget. |
| 88 | final double? width; |
| 89 | |
| 90 | /// The requested height for this widget. |
| 91 | final double? height; |
| 92 | |
| 93 | /// How the HTML element should be inscribed in the box constraining it. |
| 94 | final BoxFit? fit; |
| 95 | |
| 96 | /// How the image should be aligned in the box constraining it. |
| 97 | final AlignmentGeometry alignment; |
| 98 | |
| 99 | /// Whether or not the alignment of the image should match the text direction. |
| 100 | final bool matchTextDirection; |
| 101 | |
| 102 | @override |
| 103 | RenderObject createRenderObject(BuildContext context) { |
| 104 | return RenderWebImage( |
| 105 | image: image.htmlImage, |
| 106 | width: width, |
| 107 | height: height, |
| 108 | fit: fit, |
| 109 | alignment: alignment, |
| 110 | matchTextDirection: matchTextDirection, |
| 111 | textDirection: matchTextDirection || alignment is! Alignment |
| 112 | ? Directionality.of(context) |
| 113 | : null, |
| 114 | ); |
| 115 | } |
| 116 | |
| 117 | @override |
| 118 | void updateRenderObject(BuildContext context, RenderWebImage renderObject) { |
| 119 | renderObject |
| 120 | ..image = image.htmlImage |
| 121 | ..width = width |
| 122 | ..height = height |
| 123 | ..fit = fit |
| 124 | ..alignment = alignment |
| 125 | ..matchTextDirection = matchTextDirection |
| 126 | ..textDirection = matchTextDirection || alignment is! Alignment |
| 127 | ? Directionality.of(context) |
| 128 | : null; |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | /// Lays out and positions the child HTML element similarly to [RenderImage]. |
| 133 | class RenderWebImage extends RenderShiftedBox { |
| 134 | /// Creates a new [RenderWebImage]. |
| 135 | RenderWebImage({ |
| 136 | RenderBox? child, |
| 137 | required web.HTMLImageElement image, |
| 138 | double? width, |
| 139 | double? height, |
| 140 | BoxFit? fit, |
| 141 | AlignmentGeometry alignment = Alignment.center, |
| 142 | bool matchTextDirection = false, |
| 143 | TextDirection? textDirection, |
| 144 | }) : _image = image, |
| 145 | _width = width, |
| 146 | _height = height, |
| 147 | _fit = fit, |
| 148 | _alignment = alignment, |
| 149 | _matchTextDirection = matchTextDirection, |
| 150 | _textDirection = textDirection, |
| 151 | super(child); |
| 152 | |
| 153 | Alignment? _resolvedAlignment; |
| 154 | bool? _flipHorizontally; |
| 155 | |
| 156 | void _resolve() { |
| 157 | if (_resolvedAlignment != null) { |
| 158 | return; |
| 159 | } |
| 160 | _resolvedAlignment = alignment.resolve(textDirection); |
| 161 | _flipHorizontally = matchTextDirection && textDirection == TextDirection.rtl; |
| 162 | } |
| 163 | |
| 164 | void _markNeedResolution() { |
| 165 | _resolvedAlignment = null; |
| 166 | _flipHorizontally = null; |
| 167 | markNeedsPaint(); |
| 168 | } |
| 169 | |
| 170 | /// Whether to paint the image in the direction of the [TextDirection]. |
| 171 | /// |
| 172 | /// If this is true, then in [TextDirection.ltr] contexts, the image will be |
| 173 | /// drawn with its origin in the top left (the "normal" painting direction for |
| 174 | /// images); and in [TextDirection.rtl] contexts, the image will be drawn with |
| 175 | /// a scaling factor of -1 in the horizontal direction so that the origin is |
| 176 | /// in the top right. |
| 177 | /// |
| 178 | /// This is occasionally used with images in right-to-left environments, for |
| 179 | /// images that were designed for left-to-right locales. Be careful, when |
| 180 | /// using this, to not flip images with integral shadows, text, or other |
| 181 | /// effects that will look incorrect when flipped. |
| 182 | /// |
| 183 | /// If this is set to true, [textDirection] must not be null. |
| 184 | bool get matchTextDirection => _matchTextDirection; |
| 185 | bool _matchTextDirection; |
| 186 | set matchTextDirection(bool value) { |
| 187 | if (value == _matchTextDirection) { |
| 188 | return; |
| 189 | } |
| 190 | _matchTextDirection = value; |
| 191 | _markNeedResolution(); |
| 192 | } |
| 193 | |
| 194 | /// The text direction with which to resolve [alignment]. |
| 195 | /// |
| 196 | /// This may be changed to null, but only after the [alignment] and |
| 197 | /// [matchTextDirection] properties have been changed to values that do not |
| 198 | /// depend on the direction. |
| 199 | TextDirection? get textDirection => _textDirection; |
| 200 | TextDirection? _textDirection; |
| 201 | set textDirection(TextDirection? value) { |
| 202 | if (_textDirection == value) { |
| 203 | return; |
| 204 | } |
| 205 | _textDirection = value; |
| 206 | _markNeedResolution(); |
| 207 | } |
| 208 | |
| 209 | /// The image to display. |
| 210 | web.HTMLImageElement get image => _image; |
| 211 | web.HTMLImageElement _image; |
| 212 | set image(web.HTMLImageElement value) { |
| 213 | if (value == _image) { |
| 214 | return; |
| 215 | } |
| 216 | // If we get a clone of our image, it's the same underlying native data - |
| 217 | // return early. |
| 218 | if (value.src == _image.src) { |
| 219 | return; |
| 220 | } |
| 221 | final bool sizeChanged = |
| 222 | _image.naturalWidth != value.naturalWidth || _image.naturalHeight != value.naturalHeight; |
| 223 | _image = value; |
| 224 | markNeedsPaint(); |
| 225 | if (sizeChanged && (_width == null || _height == null)) { |
| 226 | markNeedsLayout(); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | /// If non-null, requires the image to have this width. |
| 231 | /// |
| 232 | /// If null, the image will pick a size that best preserves its intrinsic |
| 233 | /// aspect ratio. |
| 234 | double? get width => _width; |
| 235 | double? _width; |
| 236 | set width(double? value) { |
| 237 | if (value == _width) { |
| 238 | return; |
| 239 | } |
| 240 | _width = value; |
| 241 | markNeedsLayout(); |
| 242 | } |
| 243 | |
| 244 | /// If non-null, require the image to have this height. |
| 245 | /// |
| 246 | /// If null, the image will pick a size that best preserves its intrinsic |
| 247 | /// aspect ratio. |
| 248 | double? get height => _height; |
| 249 | double? _height; |
| 250 | set height(double? value) { |
| 251 | if (value == _height) { |
| 252 | return; |
| 253 | } |
| 254 | _height = value; |
| 255 | markNeedsLayout(); |
| 256 | } |
| 257 | |
| 258 | /// How to inscribe the image into the space allocated during layout. |
| 259 | /// |
| 260 | /// The default varies based on the other fields. See the discussion at |
| 261 | /// [paintImage]. |
| 262 | BoxFit? get fit => _fit; |
| 263 | BoxFit? _fit; |
| 264 | set fit(BoxFit? value) { |
| 265 | if (value == _fit) { |
| 266 | return; |
| 267 | } |
| 268 | _fit = value; |
| 269 | markNeedsPaint(); |
| 270 | } |
| 271 | |
| 272 | /// How to align the image within its bounds. |
| 273 | /// |
| 274 | /// If this is set to a text-direction-dependent value, [textDirection] must |
| 275 | /// not be null. |
| 276 | AlignmentGeometry get alignment => _alignment; |
| 277 | AlignmentGeometry _alignment; |
| 278 | set alignment(AlignmentGeometry value) { |
| 279 | if (value == _alignment) { |
| 280 | return; |
| 281 | } |
| 282 | _alignment = value; |
| 283 | _markNeedResolution(); |
| 284 | } |
| 285 | |
| 286 | /// Find a size for the render image within the given constraints. |
| 287 | /// |
| 288 | /// - The dimensions of the RenderImage must fit within the constraints. |
| 289 | /// - The aspect ratio of the RenderImage matches the intrinsic aspect |
| 290 | /// ratio of the image. |
| 291 | /// - The RenderImage's dimension are maximal subject to being smaller than |
| 292 | /// the intrinsic size of the image. |
| 293 | Size _sizeForConstraints(BoxConstraints constraints) { |
| 294 | // Folds the given |width| and |height| into |constraints| so they can all |
| 295 | // be treated uniformly. |
| 296 | constraints = BoxConstraints.tightFor(width: _width, height: _height).enforce(constraints); |
| 297 | |
| 298 | return constraints.constrainSizeAndAttemptToPreserveAspectRatio( |
| 299 | Size(_image.naturalWidth.toDouble(), _image.naturalHeight.toDouble()), |
| 300 | ); |
| 301 | } |
| 302 | |
| 303 | @override |
| 304 | double computeMinIntrinsicWidth(double height) { |
| 305 | assert(height >= 0.0); |
| 306 | if (_width == null && _height == null) { |
| 307 | return 0.0; |
| 308 | } |
| 309 | return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width; |
| 310 | } |
| 311 | |
| 312 | @override |
| 313 | double computeMaxIntrinsicWidth(double height) { |
| 314 | assert(height >= 0.0); |
| 315 | return _sizeForConstraints(BoxConstraints.tightForFinite(height: height)).width; |
| 316 | } |
| 317 | |
| 318 | @override |
| 319 | double computeMinIntrinsicHeight(double width) { |
| 320 | assert(width >= 0.0); |
| 321 | if (_width == null && _height == null) { |
| 322 | return 0.0; |
| 323 | } |
| 324 | return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height; |
| 325 | } |
| 326 | |
| 327 | @override |
| 328 | double computeMaxIntrinsicHeight(double width) { |
| 329 | assert(width >= 0.0); |
| 330 | return _sizeForConstraints(BoxConstraints.tightForFinite(width: width)).height; |
| 331 | } |
| 332 | |
| 333 | @override |
| 334 | bool hitTestSelf(Offset position) => true; |
| 335 | |
| 336 | @override |
| 337 | @protected |
| 338 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 339 | return _sizeForConstraints(constraints); |
| 340 | } |
| 341 | |
| 342 | @override |
| 343 | void performLayout() { |
| 344 | _resolve(); |
| 345 | assert(_resolvedAlignment != null); |
| 346 | assert(_flipHorizontally != null); |
| 347 | size = _sizeForConstraints(constraints); |
| 348 | |
| 349 | if (child == null) { |
| 350 | return; |
| 351 | } |
| 352 | |
| 353 | final Size inputSize = Size(image.naturalWidth.toDouble(), image.naturalHeight.toDouble()); |
| 354 | fit ??= BoxFit.scaleDown; |
| 355 | final FittedSizes fittedSizes = applyBoxFit(fit!, inputSize, size); |
| 356 | final Size childSize = fittedSizes.destination; |
| 357 | child!.layout(BoxConstraints.tight(childSize)); |
| 358 | final double halfWidthDelta = (size.width - childSize.width) / 2.0; |
| 359 | final double halfHeightDelta = (size.height - childSize.height) / 2.0; |
| 360 | final double dx = |
| 361 | halfWidthDelta + |
| 362 | (_flipHorizontally! ? -_resolvedAlignment!.x : _resolvedAlignment!.x) * halfWidthDelta; |
| 363 | final double dy = halfHeightDelta + _resolvedAlignment!.y * halfHeightDelta; |
| 364 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
| 365 | childParentData.offset = Offset(dx, dy); |
| 366 | } |
| 367 | |
| 368 | @override |
| 369 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 370 | super.debugFillProperties(properties); |
| 371 | properties.add(DiagnosticsProperty<web.HTMLImageElement>('image' , image)); |
| 372 | properties.add(DoubleProperty('width' , width, defaultValue: null)); |
| 373 | properties.add(DoubleProperty('height' , height, defaultValue: null)); |
| 374 | properties.add(EnumProperty<BoxFit>('fit' , fit, defaultValue: null)); |
| 375 | properties.add( |
| 376 | DiagnosticsProperty<AlignmentGeometry>('alignment' , alignment, defaultValue: null), |
| 377 | ); |
| 378 | } |
| 379 | } |
| 380 | |