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';
6library;
7
8import 'dart:ui_web' as ui_web;
9
10import 'package:flutter/foundation.dart';
11
12import '../painting/_web_image_info_web.dart';
13import '../rendering/box.dart';
14import '../rendering/platform_view.dart';
15import '../rendering/shifted_box.dart';
16import '../web.dart' as web;
17import 'basic.dart';
18import 'framework.dart';
19import 'platform_view.dart';
20
21/// Displays an `<img>` element with `src` set to [src].
22class 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.
68class 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].
133class 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