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 }