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 }