1 ///
2 module fluid.frame;
3 
4 import std.meta;
5 
6 import fluid.node;
7 import fluid.space;
8 import fluid.style;
9 import fluid.utils;
10 import fluid.input;
11 import fluid.structs;
12 import fluid.backend;
13 
14 import fluid.io.canvas;
15 
16 
17 @safe:
18 
19 
20 /// Make a Frame node accept nodes via drag & drop.
21 ///
22 /// Note: Currently, not all Frames support drag & drop. Using it for nodes that doesn't explicitly state support might
23 /// cause undefined behavior.
24 ///
25 /// Params:
26 ///     Node     = Require dropped nodes to be of given type.
27 ///     tags     = Restrict dropped nodes to those that have the given tag.
28 ///     selector = Selector to limit nodes that the frame accepts. Optional — Tags are often enough.
29 auto acceptDrop(tags...)(Selector selector = Selector.init)
30 if (allSatisfy!(isNodeTag, tags)) {
31 
32     struct AcceptDrop {
33 
34         Selector selector;
35 
36         void apply(Frame frame) {
37 
38             frame.dropSelector = selector;
39 
40         }
41 
42     }
43 
44     return AcceptDrop(selector.addTags!tags);
45 
46 }
47 
48 /// ditto
49 auto acceptDrop(N, tags...)()
50 if (is(N : Node) && allSatisfy!(isNodeTag, tags)) {
51 
52     auto selector = Selector(typeid(N));
53 
54     return acceptDrop!(tags)(selector);
55 
56 }
57 
58 /// Make a new vertical frame.
59 alias vframe = simpleConstructor!Frame;
60 
61 /// Make a new horizontal frame.
62 alias hframe = simpleConstructor!(Frame, (a) {
63 
64     a.directionHorizontal = true;
65 
66 });
67 
68 /// This is a frame, a stylized container for other nodes.
69 ///
70 /// Frame supports drag & drop via `acceptDrop`.
71 class Frame : Space, FluidDroppable {
72 
73     CanvasIO canvasIO;
74 
75     public {
76 
77         /// If true, a drag & drop node hovers this frame.
78         bool isDropHovered;
79 
80         /// Selector (same as in themes) used to decide which nodes can be dropped inside, defaults to none.
81         Selector dropSelector = Selector.none;
82 
83         /// Position of the cursor, indicating the area of the drop.
84         Vector2 dropCursor;
85 
86         /// Size of the droppable area.
87         Vector2 dropSize;
88 
89     }
90 
91     private {
92 
93         /// Index of the dropped node.
94         size_t _dropIndex;
95 
96         bool _queuedDrop;
97 
98         /// `dropSize` to activate after a resize.
99         Vector2 _queuedDropSize;
100 
101     }
102 
103     this(T...)(T args) {
104 
105         super(args);
106 
107     }
108 
109     protected override void resizeImpl(Vector2 availableSpace) {
110 
111         use(canvasIO);
112 
113         super.resizeImpl(availableSpace);
114 
115         // Hovered by a dragged node
116         if (_queuedDrop || isDropHovered) {
117 
118             // Apply queued changes
119             dropSize = _queuedDropSize;
120             _queuedDrop = false;
121 
122             if (isHorizontal)
123                 minSize.x += dropSize.x;
124             else
125                 minSize.y += dropSize.y;
126 
127         }
128 
129         // Clear the drop size
130         else {
131             dropSize = Vector2();
132         }
133 
134     }
135 
136     protected override void drawImpl(Rectangle outer, Rectangle inner) {
137 
138         const style = pickStyle();
139         style.drawBackground(tree.io, canvasIO, outer);
140 
141         if (isDropHovered) {
142             _dropIndex = 0;
143         }
144 
145         // Clear dropSize if dropping stopped
146         else if (dropSize != Vector2()) {
147             _queuedDrop = false;
148             updateSize();
149         }
150 
151         // Provide offset for the drop item if it's the first node
152         auto innerStart = dropOffset(start(inner));
153         inner.x = innerStart.x;
154         inner.y = innerStart.y;
155 
156         // Draw
157         super.drawImpl(outer, inner);
158 
159         // Clear dropHovered status
160         isDropHovered = false;
161 
162     }
163 
164     protected override bool hoveredImpl(Rectangle rect, Vector2 mousePosition) {
165 
166         import fluid.node;
167 
168         return Node.hoveredImpl(rect, mousePosition);
169 
170     }
171 
172     protected override Vector2 childOffset(Vector2 currentOffset, Vector2 childSpace) {
173 
174         const newOffset = super.childOffset(currentOffset, childSpace);
175 
176         // Take drop nodes into account
177         return dropOffset(newOffset);
178 
179     }
180 
181     /// Drag and drop implementation: Offset nodes to provide space for the dropped node.
182     protected Vector2 dropOffset(Vector2 offset) {
183 
184         // Ignore if nothing is dropped
185         if (!isDropHovered) return offset;
186 
187         const dropsHere = isHorizontal
188             ? dropCursor.x <= offset.x + dropSize.x
189             : dropCursor.y <= offset.y + dropSize.y;
190 
191         // Not dropping here
192         if (!dropsHere && children.length) {
193 
194             _dropIndex++;
195             return offset;
196 
197         }
198 
199         // Finish the drop event
200         isDropHovered = false;
201 
202         // Increase the offset to fit the node
203         return isHorizontal
204             ? offset + Vector2(dropSize.x, 0)
205             : offset + Vector2(0, dropSize.y);
206 
207     }
208 
209     /// Returns:
210     ///     True if the given node can be dropped into this frame.
211     ///
212     ///     No node can be dropped into a frame that is disabled.
213     bool canDrop(Node node) {
214 
215         return dropSelector.test(node)
216             && !isDisabledInherited;
217 
218     }
219 
220     void dropHover(Vector2 position, Rectangle rectangle) {
221 
222         import std.math;
223 
224         isDropHovered = true;
225         _queuedDrop = true;
226 
227         // Queue up the changes
228         dropCursor = position;
229         _queuedDropSize = size(rectangle);
230 
231         const same = isClose(dropSize.x, _queuedDropSize.x)
232             && isClose(dropSize.y, _queuedDropSize.y);
233 
234         // Updated
235         if (!same) updateSize();
236 
237     }
238 
239     void drop(Vector2 position, Rectangle rectangle, Node node) {
240 
241         import std.array;
242 
243         // Prevent overflow
244         // This might happen when rearranging an item to the end within the same container
245         if (_dropIndex > children.length)
246             _dropIndex = children.length;
247 
248         this.children.insertInPlace(_dropIndex, node);
249         updateSize();
250 
251     }
252 
253 }