1 /**
2 Copyright: Copyright (c) 2013-2017 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 
7 module anchovy.signal;
8 
9 import std.algorithm : countUntil;
10 import std.functional : toDelegate;
11 import std.traits : isDelegate, ParameterTypeTuple, isFunctionPointer;
12 
13 struct Signal(Args...)
14 {
15 	alias SlotType = void delegate(Args);
16 
17 	SlotType[] slots;
18 
19 	void emit(Args args) @trusted
20 	{
21 		foreach(slot; slots)
22 			slot(args);
23 	}
24 
25 	void connect(Slot)(Slot slot) @trusted if(is(ParameterTypeTuple!Slot == Args))
26 	{
27 		static if(isDelegate!Slot)
28 		{
29 			slots ~= slot;
30 		}
31 		else static if(isFunctionPointer!Slot)
32 		{
33 			slots ~= toDelegate(slot);
34 		}
35 	}
36 
37 	void disconnect(Slot)(Slot slot) @trusted
38 	{
39 		static if(isDelegate!Slot)
40 		{
41 			auto haystackPos = countUntil(slots, slot);
42 
43 			if(haystackPos >= 0)
44 			{
45 				slots = slots[0..haystackPos] ~ slots[haystackPos+1 .. $];
46 			}
47 		}
48 		else static if(isFunctionPointer!Slot)
49 		{
50 			// struct from functional toDelegate
51 			static struct DelegateFields {
52 	            union {
53 	                SlotType del;
54 	                struct {
55 	                    void* contextPtr;
56 	                    void* funcPtr;
57 	                }
58 	            }
59 	        }
60 
61 	        auto haystackPos = countUntil!((SlotType _slot, ){return (cast(DelegateFields)_slot).contextPtr == slot;})(slots);
62 
63 			if(haystackPos >= 0)
64 			{
65 				slots = slots[0..haystackPos] ~ slots[haystackPos+1 .. $];
66 			}
67 		}
68 
69 	}
70 
71 	void disconnectAll() @trusted
72 	{
73 		slots = [];
74 	}
75 }
76 
77 // Test for signal with 0 arguments
78 unittest
79 {
80 	Signal!() test1; // Signal for slots not taking any parameters.
81 
82 	auto num = 0;
83 	auto slot = (){num += 1;}; // Slot is plain delegate.
84 	test1.connect(slot);
85 	assert(num == 0); // Slot doesn't gets called upon connecting.
86 
87 	test1.emit();
88 	assert(num == 1); // Each connected slot is called only once.
89 
90 	test1.disconnect(slot);
91 	assert(num == 1); // Doesn't called upon disconnecting.
92 
93 	test1.emit();
94 	assert(num == 1);
95 }
96 
97 // Test for signal with 1 argument
98 unittest
99 {
100 	// Slot that takes one argument.
101 	// Slots can have any number of parameters.
102 	Signal!int test2;
103 
104 	auto num = 0;
105 	auto slot = (int increment){num += increment;};
106 	test2.connect(slot);
107 	assert(num == 0);
108 
109 	test2.emit(3);
110 	assert(num == 3);
111 
112 	test2.disconnect(slot);
113 	assert(num == 3);
114 
115 	test2.emit(4);
116 	assert(num == 3);
117 }
118 
119 // Test for multiple slots
120 unittest
121 {
122 	Signal!int test3;
123 
124 	auto num = 0;
125 	auto slot1 = (int inc){num += inc;};
126 	auto slot2 = (int mult){num *= mult;};
127 
128 	test3.connect(slot1);
129 	test3.connect(slot2);
130 	assert(num == 0);
131 
132 	test3.emit(2);
133 	assert(num == 4);
134 
135 	test3.connect(slot1);
136 	test3.emit(3);
137 	assert(num == (4 + 3) * 3 + 3); // 24
138 
139 	test3.disconnect(slot1);
140 	test3.emit(2);
141 	assert(num == 24 * 2 + 2); // 50
142 
143 	test3.disconnectAll();
144 	test3.emit(4);
145 	assert(num == 50);
146 }
147 
148 // Test for static slots
149 unittest
150 {
151 	Signal!(int*, int) test4;
152 
153 	auto num = 0;
154 	// Testing static functions.
155 	static void staticSlot(int* num, int inc){*num += inc;}
156 
157 	test4.connect(&staticSlot);
158 	assert(num == 0);
159 
160 	test4.emit(&num, 2);
161 	assert(num == 2);
162 
163 	test4.disconnect(&staticSlot);
164 	test4.emit(&num, 2);
165 	assert(num == 2);
166 }