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