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 }