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 }