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 import fluid.io.file;
10 import fluid.io.canvas;
11 import fluid.io.image_load;
12 
13 @safe:
14 
15 alias imageView = simpleConstructor!ImageView;
16 
17 auto autoExpand(bool value = true) {
18 
19     static struct AutoExpand {
20 
21         bool value;
22 
23         void apply(ImageView node) {
24             node.isAutoExpand = value;
25         }
26 
27     }
28 
29     return AutoExpand(value);
30 
31 }
32 
33 /// A node specifically to display images.
34 ///
35 /// The image will automatically scale to fit available space. It will keep aspect ratio by default and will be
36 /// displayed in the middle of the available box.
37 class ImageView : Node {
38 
39     CanvasIO canvasIO;
40     FileIO fileIO;
41     ImageLoadIO imageLoadIO;
42 
43     public {
44 
45         /// Image this node should display, if any. Requires an active `CanvasIO` to display.
46         DrawableImage image;
47 
48         /// If true, size of this imageView is adjusted automatically. Changes made to `minSize` will be reversed on
49         /// size update.
50         bool isSizeAutomatic;
51 
52         /// Experimental. Acquire space from the parent to display the largest image while preserving aspect ratio.
53         /// May not work if there's multiple similarly sized nodes in the same container.
54         bool isAutoExpand;
55 
56     }
57 
58     protected {
59 
60         /// Texture for this node.
61         Texture _texture;
62 
63         /// If set, path in the filesystem the texture is to be loaded from.
64         string _texturePath;
65 
66         /// Rectangle occupied by this node after all calculations.
67         Rectangle _targetArea;
68 
69     }
70 
71     /// Set to true if the image view owns the texture and manages its ownership.
72     private bool _isOwner;
73 
74     /// Create an image node from given texture or filename.
75     ///
76     /// Note, if a string is given, the texture will be loaded when resizing. This ensures a Fluid backend is available
77     /// to load the texture.
78     ///
79     /// Params:
80     ///     source  = `Texture` struct to use, or a filename to load from.
81     ///     minSize = Minimum size of the node. Defaults to image size.
82     this(T)(T source, Vector2 minSize) {
83 
84         super.minSize = minSize;
85         this.texture = source;
86 
87     }
88 
89     /// ditto
90     this(T)(T source) {
91 
92         this.texture = source;
93         this.isSizeAutomatic = true;
94 
95     }
96 
97     /// Create an image node using given image.
98     /// Params:
99     ///     image   = Image to load.
100     ///     minSize = Minimum size of the node. Defaults to image size.
101     this(Image image, Vector2 minSize) {
102 
103         super.minSize = minSize;
104         this.image = DrawableImage(image);
105 
106     }
107 
108     /// ditto
109     this(Image image) {
110 
111         this.image = DrawableImage(image);
112         this.isSizeAutomatic = true;
113 
114     }
115 
116     ~this() {
117 
118         clear();
119 
120     }
121 
122     @property {
123 
124         /// Set the texture.
125         Texture texture(Texture texture) {
126 
127             clear();
128             _isOwner = false;
129             _texturePath = null;
130 
131             return this._texture = texture;
132 
133         }
134 
135         Image texture(Image image) {
136 
137             clear();
138             updateSize();
139             this.image = image;
140 
141             return image;
142         }
143 
144         /// Load the texture from a filename.
145         string texture(string filename) @trusted {
146 
147             import std.string : toStringz;
148 
149             _texturePath = filename;
150 
151             if (tree && !canvasIO) {
152 
153                 clear();
154                 _texture = tree.io.loadTexture(filename);
155                 _isOwner = true;
156 
157             }
158 
159             updateSize();
160 
161             return filename;
162 
163         }
164 
165         /// Get the current texture.
166         const(Texture) texture() const {
167 
168             return _texture;
169 
170         }
171 
172     }
173 
174     /// Release ownership over the displayed texture.
175     ///
176     /// Keep the texture alive as long as it's used by this `imageView`, free it manually using the `destroy()` method.
177     Texture release() {
178 
179         _isOwner = false;
180         return _texture;
181 
182     }
183 
184     /// Remove any texture if attached.
185     void clear() @trusted scope {
186 
187         // Free the texture
188         if (_isOwner) {
189 
190             _texture.destroy();
191 
192         }
193 
194         // Remove the texture
195         _texture = texture.init;
196         _texturePath = null;
197 
198         // Remove the image
199         image = Image.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         use(canvasIO);
224         use(fileIO);
225         use(imageLoadIO);
226 
227         // New I/O system
228         if (canvasIO) {
229 
230             // Load an image from the filesystem if no image is already loaded
231             if (image == Image.init && _texturePath != "" && fileIO && imageLoadIO) {
232 
233                 auto file = fileIO.loadFile(_texturePath);
234                 image = imageLoadIO.loadImage(file);
235 
236             }
237 
238             // Load the image
239             load(canvasIO, image);
240 
241             // Adjust size
242             if (isSizeAutomatic) {
243 
244                 const viewportSize = image.viewportSize(canvasIO.dpi);
245 
246                 // No image loaded, shrink to nothingness
247                 if (image == Image.init) {
248                     minSize = Vector2(0, 0);
249                 }
250 
251                 else if (isAutoExpand) {
252                     minSize = fitInto(viewportSize, space);
253                 }
254 
255                 else {
256                     minSize = viewportSize;
257                 }
258 
259             }
260 
261         }
262 
263         // Old backend
264         else {
265 
266             // Lazy-load the texture if the backend wasn't present earlier
267             if (_texture == _texture.init && _texturePath) {
268                 _texture = tree.io.loadTexture(_texturePath);
269                 _isOwner = true;
270             }
271             else if (_texture == texture.init && image != Image.init) {
272                 _texture = tree.io.loadTexture(image);
273                 _isOwner = true;
274             }
275 
276             // Adjust size
277             if (isSizeAutomatic) {
278 
279                 // No texture loaded, shrink to nothingness
280                 if (_texture is _texture.init) {
281                     minSize = Vector2(0, 0);
282                 }
283 
284                 else if (isAutoExpand) {
285                     minSize = fitInto(texture.viewportSize, space);
286                 }
287 
288                 else {
289                     minSize = texture.viewportSize;
290                 }
291 
292             }
293 
294         }
295 
296     }
297 
298     override protected void drawImpl(Rectangle, Rectangle inner) @trusted {
299 
300         import std.algorithm : min;
301 
302         if (canvasIO) {
303 
304             // Ignore if there is no texture to draw
305             if (image == Image.init) return;
306 
307             const size = image.viewportSize(canvasIO.dpi)
308                 .fitInto(inner.size);
309             const position = center(inner) - size/2;
310 
311             _targetArea = Rectangle(position.tupleof, size.tupleof);
312             image.draw(_targetArea);
313 
314         }
315 
316         else {
317 
318             // Ignore if there is no texture to draw
319             if (texture.id <= 0) return;
320 
321             const size     = fitInto(texture.viewportSize, inner.size);
322             const position = center(inner) - size/2;
323 
324             _targetArea = Rectangle(position.tupleof, size.tupleof);
325             _texture.draw(_targetArea);
326 
327         }
328 
329     }
330 
331     override protected bool hoveredImpl(Rectangle, Vector2 mouse) const {
332 
333         return _targetArea.contains(mouse);
334 
335     }
336 
337 }
338 
339 /// Returns: A vector smaller than `space` using the same aspect ratio as `reference`.
340 /// Params:
341 ///     reference = Vector to use the aspect ratio of.
342 ///     space     = Available space; maximum size on each axis for the result vector.
343 Vector2 fitInto(Vector2 reference, Vector2 space) {
344 
345     import std.algorithm : min;
346 
347     const scale = min(
348         space.x / reference.x,
349         space.y / reference.y,
350     );
351 
352     return reference * scale;
353 
354 }