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