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