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