1 /// 2 module fluid.field_slot; 3 4 import fluid.node; 5 import fluid.utils; 6 import fluid.input; 7 import fluid.actions; 8 import fluid.backend; 9 10 11 @safe: 12 13 14 /// A field slot is a node meant to hold an input node along with associated nodes, like labels. It's functionally 15 /// equivalent to the [`<label>` element in HTML](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label). 16 /// 17 /// Fields expand the interactable (clickable) area of input nodes by other nodes that are placed inside the slot. For 18 /// example, in the code snippet below, if the user clicks on the "username" label, the text input underneath will gain 19 /// focus. 20 /// 21 /// --- 22 /// fieldSlot!vframe( 23 /// label("Username"), 24 /// textInput(), 25 /// ) 26 /// --- 27 alias fieldSlot(alias node) = simpleConstructor!(FieldSlot, node); 28 29 /// ditto 30 class FieldSlot(T : Node) : T, FluidHoverable { 31 32 mixin makeHoverable; 33 mixin enableInputActions; 34 35 this(Args...)(Args args) { 36 super(args); 37 } 38 39 /// Pass focus to the field contained by this slot. 40 @(FluidInputAction.press) 41 void focus() { 42 43 auto action = this.focusRecurseChildren(); 44 45 // Press the target when found 46 action.finished = (node) { 47 if (node) { 48 node.runInputAction!(FluidInputAction.press); 49 } 50 }; 51 52 } 53 54 override void drawImpl(Rectangle outer, Rectangle inner) { 55 56 const previousHover = tree.hover; 57 58 // Draw children 59 super.drawImpl(outer, inner); 60 61 // Test if hover has switched to any of them 62 const isChildHovered = tree.hover !is previousHover; 63 64 // If the node doesn't handle hover itself, take over 65 // (pun not intended) 66 if (isChildHovered && !cast(FluidHoverable) tree.hover) { 67 68 tree.hover = this; 69 70 } 71 72 } 73 74 // implements FluidHoverable 75 override bool isHovered() const { 76 return tree.hover is this 77 || super.isHovered(); 78 } 79 80 // implements FluidHoverable 81 void mouseImpl() { 82 83 } 84 85 } 86 87 unittest { 88 89 import fluid.frame; 90 import fluid.label; 91 import fluid.structs; 92 import fluid.text_input; 93 94 TextInput input; 95 96 auto io = new HeadlessBackend; 97 auto root = fieldSlot!vframe( 98 layout!"fill", 99 label("Hello, World!"), 100 input = textInput(), 101 ); 102 103 root.io = io; 104 root.draw(); 105 106 assert(!input.isFocused); 107 108 // In this case, clicking anywhere should give the textInput focus 109 io.nextFrame; 110 io.mousePosition = Vector2(200, 200); 111 io.press; 112 root.draw(); 113 114 assert(root.tree.hover is root); 115 116 // Trigger the event 117 io.nextFrame; 118 io.release; 119 root.draw(); 120 121 // Focus should be transferred once actions have been processed 122 io.nextFrame; 123 root.draw(); 124 125 assert(input.isFocused); 126 127 } 128 129 unittest { 130 131 import fluid.space; 132 import fluid.label; 133 import fluid.structs; 134 import fluid.text_input; 135 import fluid.default_theme; 136 137 Label theLabel; 138 TextInput input; 139 140 // This time around use a vspace, so it won't trigger hover events when pressed outside 141 auto io = new HeadlessBackend; 142 auto root = fieldSlot!vspace( 143 layout!"fill", 144 nullTheme, 145 theLabel = label("Hello, World!"), 146 input = textInput(), 147 ); 148 149 root.io = io; 150 root.draw(); 151 152 assert(!input.isFocused); 153 154 // Hover outside 155 io.nextFrame; 156 io.mousePosition = Vector2(500, 500); 157 root.draw(); 158 159 assert(root.tree.hover is null); 160 161 // Hover the label 162 io.nextFrame; 163 io.mousePosition = Vector2(5, 5); 164 io.press; 165 root.draw(); 166 167 // The root should take the hover 168 assert(theLabel.isHovered); 169 assert(root.tree.hover is root); 170 171 // Trigger the event 172 io.nextFrame; 173 io.release; 174 root.draw(); 175 176 // Focus should be transferred once actions have been processed 177 io.nextFrame; 178 root.draw(); 179 180 assert(input.isFocused); 181 182 }