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 @safe: 10 11 alias imageView = simpleConstructor!ImageView; 12 13 auto autoExpand(bool value = true) { 14 15 static struct AutoExpand { 16 17 bool value; 18 19 void apply(ImageView node) { 20 node.isAutoExpand = value; 21 } 22 23 } 24 25 return AutoExpand(value); 26 27 28 } 29 30 /// A node specifically to display images. 31 /// 32 /// The image will automatically scale to fit available space. It will keep aspect ratio by default and will be 33 /// displayed in the middle of the available box. 34 class ImageView : Node { 35 36 public { 37 38 /// If true, size of this imageView is adjusted automatically. Changes made to `minSize` will be reversed on 39 /// size update. 40 bool isSizeAutomatic; 41 42 /// Experimental. Acquire space from the parent to display the largest image while preserving aspect ratio. 43 /// May not work if there's multiple similarly sized nodes in the same container. 44 bool isAutoExpand; 45 46 } 47 48 protected { 49 50 /// Texture for this node. 51 Texture _texture; 52 53 /// If set, path in the filesystem the texture is to be loaded from. 54 string _texturePath; 55 56 /// Rectangle occupied by this node after all calculations. 57 Rectangle _targetArea; 58 59 } 60 61 /// Set to true if the image view owns the texture and manages its ownership. 62 private bool _isOwner; 63 64 /// Create an image node from given texture or filename. 65 /// 66 /// Note, if a string is given, the texture will be loaded when resizing. This ensures a Fluid backend is available 67 /// to load the texture. 68 /// 69 /// Params: 70 /// source = `Texture` struct to use, or a filename to load from. 71 /// minSize = Minimum size of the node. Defaults to image size. 72 this(T)(T source, Vector2 minSize) { 73 74 super.minSize = minSize; 75 this.texture = source; 76 77 } 78 79 /// ditto 80 this(T)(T source) { 81 82 this.texture = source; 83 this.isSizeAutomatic = true; 84 85 } 86 87 ~this() { 88 89 clear(); 90 91 } 92 93 @property { 94 95 /// Set the texture. 96 Texture texture(Texture texture) { 97 98 clear(); 99 _isOwner = false; 100 _texturePath = null; 101 102 return this._texture = texture; 103 104 } 105 106 Image texture(Image image) @system { 107 108 clear(); 109 _texture = tree.io.loadTexture(image); 110 _isOwner = true; 111 112 return image; 113 } 114 115 /// Load the texture from a filename. 116 string texture(string filename) @trusted { 117 118 import std.string : toStringz; 119 120 _texturePath = filename; 121 122 if (tree) { 123 124 clear(); 125 _texture = tree.io.loadTexture(filename); 126 _isOwner = true; 127 128 } 129 130 updateSize(); 131 132 return filename; 133 134 } 135 136 @system unittest { 137 138 // TODO test for keeping aspect ratio 139 auto io = new HeadlessBackend(Vector2(1000, 1000)); 140 auto root = imageView(.nullTheme, "logo.png"); 141 142 // The texture will lazy-load 143 assert(root.texture == Texture.init); 144 145 root.io = io; 146 root.draw(); 147 148 // Texture should be loaded by now 149 assert(root.texture != Texture.init); 150 151 io.assertTexture(root.texture, Vector2(0, 0), color!"fff"); 152 153 } 154 155 /// Get the current texture. 156 const(Texture) texture() const { 157 158 return _texture; 159 160 } 161 162 } 163 164 /// Release ownership over the displayed texture. 165 /// 166 /// Keep the texture alive as long as it's used by this `imageView`, free it manually using the `destroy()` method. 167 Texture release() { 168 169 _isOwner = false; 170 return _texture; 171 172 } 173 174 /// Remove any texture if attached. 175 void clear() @trusted scope { 176 177 // Free the texture 178 if (_isOwner) { 179 180 _texture.destroy(); 181 182 } 183 184 // Remove the texture 185 _texture = texture.init; 186 187 } 188 189 /// Minimum size of the image. 190 @property 191 ref inout(Vector2) minSize() inout { 192 193 return super.minSize; 194 195 } 196 197 /// Area on the screen the image was last drawn to. 198 @property 199 Rectangle targetArea() const { 200 201 return _targetArea; 202 203 } 204 205 override protected void resizeImpl(Vector2 space) @trusted { 206 207 import std.algorithm : min; 208 209 // Lazy-load the texture if the backend wasn't present earlier 210 if (_texture == _texture.init && _texturePath) { 211 212 _texture = tree.io.loadTexture(_texturePath); 213 _isOwner = true; 214 215 } 216 217 // Adjust size 218 if (isSizeAutomatic) { 219 220 // No texture loaded, shrink to nothingness 221 if (_texture is _texture.init) { 222 minSize = Vector2(0, 0); 223 } 224 225 else if (isAutoExpand) { 226 minSize = fitInto(texture.viewportSize, space); 227 } 228 229 else { 230 minSize = texture.viewportSize; 231 } 232 233 } 234 235 } 236 237 override protected void drawImpl(Rectangle, Rectangle rect) @trusted { 238 239 import std.algorithm : min; 240 241 // Ignore if there is no texture to draw 242 if (texture.id <= 0) return; 243 244 const size = fitInto(texture.viewportSize, rect.size); 245 const position = center(rect) - size/2; 246 247 _targetArea = Rectangle(position.tupleof, size.tupleof); 248 _texture.draw(_targetArea); 249 250 } 251 252 override protected bool hoveredImpl(Rectangle, Vector2 mouse) const { 253 254 return _targetArea.contains(mouse); 255 256 } 257 258 } 259 260 /// Returns: A vector smaller than `space` using the same aspect ratio as `reference`. 261 /// Params: 262 /// reference = Vector to use the aspect ratio of. 263 /// space = Available space; maximum size on each axis for the result vector. 264 Vector2 fitInto(Vector2 reference, Vector2 space) { 265 266 import std.algorithm : min; 267 268 const scale = min( 269 space.x / reference.x, 270 space.y / reference.y, 271 ); 272 273 return reference * scale; 274 275 }