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 }