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 }