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 version (Have_raylib_d) { 154 import std.string: toStringz; 155 raylib.Image LoadImage(string path) => raylib.LoadImage(path.toStringz); 156 raylib.Texture LoadTexture(string path) => raylib.LoadTexture(path.toStringz); 157 void InitWindow() => raylib.InitWindow(80, 80, ""); 158 void CloseWindow() => raylib.CloseWindow(); 159 160 InitWindow; 161 raylib.Texture rayTexture = LoadTexture("logo.png"); 162 fluid.Texture texture = rayTexture.toFluid; 163 164 io.assertTexture(root.texture, Vector2(0, 0), color!"fff"); 165 CloseWindow; 166 } 167 } 168 169 /// Get the current texture. 170 const(Texture) texture() const { 171 172 return _texture; 173 174 } 175 176 } 177 178 /// Release ownership over the displayed texture. 179 /// 180 /// Keep the texture alive as long as it's used by this `imageView`, free it manually using the `destroy()` method. 181 Texture release() { 182 183 _isOwner = false; 184 return _texture; 185 186 } 187 188 /// Remove any texture if attached. 189 void clear() @trusted scope { 190 191 // Free the texture 192 if (_isOwner) { 193 194 _texture.destroy(); 195 196 } 197 198 // Remove the texture 199 _texture = texture.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 // Lazy-load the texture if the backend wasn't present earlier 224 if (_texture == _texture.init && _texturePath) { 225 226 _texture = tree.io.loadTexture(_texturePath); 227 _isOwner = true; 228 229 } 230 231 // Adjust size 232 if (isSizeAutomatic) { 233 234 // No texture loaded, shrink to nothingness 235 if (_texture is _texture.init) { 236 minSize = Vector2(0, 0); 237 } 238 239 else if (isAutoExpand) { 240 minSize = fitInto(texture.viewportSize, space); 241 } 242 243 else { 244 minSize = texture.viewportSize; 245 } 246 247 } 248 249 } 250 251 override protected void drawImpl(Rectangle, Rectangle rect) @trusted { 252 253 import std.algorithm : min; 254 255 // Ignore if there is no texture to draw 256 if (texture.id <= 0) return; 257 258 const size = fitInto(texture.viewportSize, rect.size); 259 const position = center(rect) - size/2; 260 261 _targetArea = Rectangle(position.tupleof, size.tupleof); 262 _texture.draw(_targetArea); 263 264 } 265 266 override protected bool hoveredImpl(Rectangle, Vector2 mouse) const { 267 268 return _targetArea.contains(mouse); 269 270 } 271 272 } 273 274 /// Returns: A vector smaller than `space` using the same aspect ratio as `reference`. 275 /// Params: 276 /// reference = Vector to use the aspect ratio of. 277 /// space = Available space; maximum size on each axis for the result vector. 278 Vector2 fitInto(Vector2 reference, Vector2 space) { 279 280 import std.algorithm : min; 281 282 const scale = min( 283 space.x / reference.x, 284 space.y / reference.y, 285 ); 286 287 return reference * scale; 288 289 }