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