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         }
154 
155         /// Get the current texture.
156         const(Texture) texture() const {
157 
158             return _texture;
159 
160         }
161 
162     }
163 
164     /// Release ownership over the displayed texture.
165     ///
166     /// Keep the texture alive as long as it's used by this `imageView`, free it manually using the `destroy()` method.
167     Texture release() {
168 
169         _isOwner = false;
170         return _texture;
171 
172     }
173 
174     /// Remove any texture if attached.
175     void clear() @trusted scope {
176 
177         // Free the texture
178         if (_isOwner) {
179 
180             _texture.destroy();
181 
182         }
183 
184         // Remove the texture
185         _texture = texture.init;
186 
187     }
188 
189     /// Minimum size of the image.
190     @property
191     ref inout(Vector2) minSize() inout {
192 
193         return super.minSize;
194 
195     }
196 
197     /// Area on the screen the image was last drawn to.
198     @property
199     Rectangle targetArea() const {
200 
201         return _targetArea;
202 
203     }
204     
205     override protected void resizeImpl(Vector2 space) @trusted {
206 
207         import std.algorithm : min;
208 
209         // Lazy-load the texture if the backend wasn't present earlier
210         if (_texture == _texture.init && _texturePath) {
211 
212             _texture = tree.io.loadTexture(_texturePath);
213             _isOwner = true;
214 
215         }
216 
217         // Adjust size
218         if (isSizeAutomatic) {
219 
220             // No texture loaded, shrink to nothingness
221             if (_texture is _texture.init) {
222                 minSize = Vector2(0, 0);
223             }
224 
225             else if (isAutoExpand) {
226                 minSize = fitInto(texture.viewportSize, space);
227             }
228 
229             else {
230                 minSize = texture.viewportSize;
231             }
232 
233         }
234 
235     }
236 
237     override protected void drawImpl(Rectangle, Rectangle rect) @trusted {
238 
239         import std.algorithm : min;
240 
241         // Ignore if there is no texture to draw
242         if (texture.id <= 0) return;
243 
244         const size     = fitInto(texture.viewportSize, rect.size);
245         const position = center(rect) - size/2;
246 
247         _targetArea = Rectangle(position.tupleof, size.tupleof);
248         _texture.draw(_targetArea);
249 
250     }
251 
252     override protected bool hoveredImpl(Rectangle, Vector2 mouse) const {
253 
254         return _targetArea.contains(mouse);
255 
256     }
257 
258 }
259 
260 /// Returns: A vector smaller than `space` using the same aspect ratio as `reference`.
261 /// Params:
262 ///     reference = Vector to use the aspect ratio of.
263 ///     space     = Available space; maximum size on each axis for the result vector.
264 Vector2 fitInto(Vector2 reference, Vector2 space) {
265 
266     import std.algorithm : min;
267 
268     const scale = min(
269         space.x / reference.x,
270         space.y / reference.y,
271     );
272 
273     return reference * scale;
274 
275 }