1 /// 2 module fluid.image_view; 3 4 import fluid.node; 5 import fluid.utils; 6 import fluid.style; 7 import fluid.backend; 8 9 import fluid.io.file; 10 import fluid.io.canvas; 11 import fluid.io.image_load; 12 13 @safe: 14 15 alias imageView = simpleConstructor!ImageView; 16 17 auto autoExpand(bool value = true) { 18 19 static struct AutoExpand { 20 21 bool value; 22 23 void apply(ImageView node) { 24 node.isAutoExpand = value; 25 } 26 27 } 28 29 return AutoExpand(value); 30 31 } 32 33 /// A node specifically to display images. 34 /// 35 /// The image will automatically scale to fit available space. It will keep aspect ratio by default and will be 36 /// displayed in the middle of the available box. 37 class ImageView : Node { 38 39 CanvasIO canvasIO; 40 FileIO fileIO; 41 ImageLoadIO imageLoadIO; 42 43 public { 44 45 /// Image this node should display, if any. Requires an active `CanvasIO` to display. 46 DrawableImage image; 47 48 /// If true, size of this imageView is adjusted automatically. Changes made to `minSize` will be reversed on 49 /// size update. 50 bool isSizeAutomatic; 51 52 /// Experimental. Acquire space from the parent to display the largest image while preserving aspect ratio. 53 /// May not work if there's multiple similarly sized nodes in the same container. 54 bool isAutoExpand; 55 56 } 57 58 protected { 59 60 /// Texture for this node. 61 Texture _texture; 62 63 /// If set, path in the filesystem the texture is to be loaded from. 64 string _texturePath; 65 66 /// Rectangle occupied by this node after all calculations. 67 Rectangle _targetArea; 68 69 } 70 71 /// Set to true if the image view owns the texture and manages its ownership. 72 private bool _isOwner; 73 74 /// Create an image node from given texture or filename. 75 /// 76 /// Note, if a string is given, the texture will be loaded when resizing. This ensures a Fluid backend is available 77 /// to load the texture. 78 /// 79 /// Params: 80 /// source = `Texture` struct to use, or a filename to load from. 81 /// minSize = Minimum size of the node. Defaults to image size. 82 this(T)(T source, Vector2 minSize) { 83 84 super.minSize = minSize; 85 this.texture = source; 86 87 } 88 89 /// ditto 90 this(T)(T source) { 91 92 this.texture = source; 93 this.isSizeAutomatic = true; 94 95 } 96 97 /// Create an image node using given image. 98 /// Params: 99 /// image = Image to load. 100 /// minSize = Minimum size of the node. Defaults to image size. 101 this(Image image, Vector2 minSize) { 102 103 super.minSize = minSize; 104 this.image = DrawableImage(image); 105 106 } 107 108 /// ditto 109 this(Image image) { 110 111 this.image = DrawableImage(image); 112 this.isSizeAutomatic = true; 113 114 } 115 116 ~this() { 117 118 clear(); 119 120 } 121 122 @property { 123 124 /// Set the texture. 125 Texture texture(Texture texture) { 126 127 clear(); 128 _isOwner = false; 129 _texturePath = null; 130 131 return this._texture = texture; 132 133 } 134 135 Image texture(Image image) { 136 137 clear(); 138 updateSize(); 139 this.image = image; 140 141 return image; 142 } 143 144 /// Load the texture from a filename. 145 string texture(string filename) @trusted { 146 147 import std.string : toStringz; 148 149 _texturePath = filename; 150 151 if (tree && !canvasIO) { 152 153 clear(); 154 _texture = tree.io.loadTexture(filename); 155 _isOwner = true; 156 157 } 158 159 updateSize(); 160 161 return filename; 162 163 } 164 165 /// Get the current texture. 166 const(Texture) texture() const { 167 168 return _texture; 169 170 } 171 172 } 173 174 /// Release ownership over the displayed texture. 175 /// 176 /// Keep the texture alive as long as it's used by this `imageView`, free it manually using the `destroy()` method. 177 Texture release() { 178 179 _isOwner = false; 180 return _texture; 181 182 } 183 184 /// Remove any texture if attached. 185 void clear() @trusted scope { 186 187 // Free the texture 188 if (_isOwner) { 189 190 _texture.destroy(); 191 192 } 193 194 // Remove the texture 195 _texture = texture.init; 196 _texturePath = null; 197 198 // Remove the image 199 image = Image.init; 200 201 } 202 203 /// Minimum size of the image. 204 @property 205 ref inout(Vector2) minSize() inout { 206 207 return super.minSize; 208 209 } 210 211 /// Area on the screen the image was last drawn to. 212 @property 213 Rectangle targetArea() const { 214 215 return _targetArea; 216 217 } 218 219 override protected void resizeImpl(Vector2 space) @trusted { 220 221 import std.algorithm : min; 222 223 use(canvasIO); 224 use(fileIO); 225 use(imageLoadIO); 226 227 // New I/O system 228 if (canvasIO) { 229 230 // Load an image from the filesystem if no image is already loaded 231 if (image == Image.init && _texturePath != "" && fileIO && imageLoadIO) { 232 233 auto file = fileIO.loadFile(_texturePath); 234 image = imageLoadIO.loadImage(file); 235 236 } 237 238 // Load the image 239 load(canvasIO, image); 240 241 // Adjust size 242 if (isSizeAutomatic) { 243 244 const viewportSize = image.viewportSize(canvasIO.dpi); 245 246 // No image loaded, shrink to nothingness 247 if (image == Image.init) { 248 minSize = Vector2(0, 0); 249 } 250 251 else if (isAutoExpand) { 252 minSize = fitInto(viewportSize, space); 253 } 254 255 else { 256 minSize = viewportSize; 257 } 258 259 } 260 261 } 262 263 // Old backend 264 else { 265 266 // Lazy-load the texture if the backend wasn't present earlier 267 if (_texture == _texture.init && _texturePath) { 268 _texture = tree.io.loadTexture(_texturePath); 269 _isOwner = true; 270 } 271 else if (_texture == texture.init && image != Image.init) { 272 _texture = tree.io.loadTexture(image); 273 _isOwner = true; 274 } 275 276 // Adjust size 277 if (isSizeAutomatic) { 278 279 // No texture loaded, shrink to nothingness 280 if (_texture is _texture.init) { 281 minSize = Vector2(0, 0); 282 } 283 284 else if (isAutoExpand) { 285 minSize = fitInto(texture.viewportSize, space); 286 } 287 288 else { 289 minSize = texture.viewportSize; 290 } 291 292 } 293 294 } 295 296 } 297 298 override protected void drawImpl(Rectangle, Rectangle inner) @trusted { 299 300 import std.algorithm : min; 301 302 if (canvasIO) { 303 304 // Ignore if there is no texture to draw 305 if (image == Image.init) return; 306 307 const size = image.viewportSize(canvasIO.dpi) 308 .fitInto(inner.size); 309 const position = center(inner) - size/2; 310 311 _targetArea = Rectangle(position.tupleof, size.tupleof); 312 image.draw(_targetArea); 313 314 } 315 316 else { 317 318 // Ignore if there is no texture to draw 319 if (texture.id <= 0) return; 320 321 const size = fitInto(texture.viewportSize, inner.size); 322 const position = center(inner) - size/2; 323 324 _targetArea = Rectangle(position.tupleof, size.tupleof); 325 _texture.draw(_targetArea); 326 327 } 328 329 } 330 331 override protected bool hoveredImpl(Rectangle, Vector2 mouse) const { 332 333 return _targetArea.contains(mouse); 334 335 } 336 337 } 338 339 /// Returns: A vector smaller than `space` using the same aspect ratio as `reference`. 340 /// Params: 341 /// reference = Vector to use the aspect ratio of. 342 /// space = Available space; maximum size on each axis for the result vector. 343 Vector2 fitInto(Vector2 reference, Vector2 space) { 344 345 import std.algorithm : min; 346 347 const scale = min( 348 space.x / reference.x, 349 space.y / reference.y, 350 ); 351 352 return reference * scale; 353 354 }