1 module modernui.collections; 2 3 import std.range; 4 import std.container.array; 5 import std.traits; 6 import modernui.rx; 7 8 struct ReadOnlyListRange(E) 9 { 10 Array!(E).Range range; 11 12 this(Array!(E).Range r) 13 { 14 range = r; 15 } 16 17 @property ReadOnlyListRange save() 18 { 19 return this; 20 } 21 22 @property bool empty() @safe pure nothrow const 23 { 24 return range.empty(); 25 } 26 27 @property size_t length() @safe pure nothrow const 28 { 29 return range.length(); 30 } 31 32 alias opDollar = length; 33 34 @property E front() 35 { 36 return range.front(); 37 } 38 39 @property E back() 40 { 41 return range.back(); 42 } 43 44 void popFront() @safe pure nothrow 45 { 46 range.popFront(); 47 } 48 49 void popBack() @safe pure nothrow 50 { 51 range.popBack(); 52 } 53 } 54 55 class ReadOnlyList(T) 56 { 57 private Array!T impl; 58 59 alias Range = ReadOnlyListRange!T; 60 61 this() 62 { 63 impl = Array!T(); 64 } 65 66 this(U)(U[] values...) if (isImplicitlyConvertible!(U, T)) 67 { 68 impl = Array!T(values); 69 } 70 71 this(Stuff)(Stuff stuff) if (isInputRange!Stuff && isImplicitlyConvertible!(ElementType!Stuff, T) && !is(Stuff == T[])) 72 { 73 impl = Array!(stuff); 74 } 75 76 @property size_t length() const { return impl.length; } 77 78 bool contains(T val) const 79 { 80 foreach(v; impl) 81 { 82 if(v == val) return true; 83 } 84 return false; 85 } 86 87 T opIndex(size_t index) 88 { 89 return impl[index]; 90 } 91 92 Range opSlice() { return Range(impl[]); } 93 94 Range opSlice(size_t begin, size_t excl_end) 95 { 96 return Range(impl[begin..excl_end]); 97 } 98 99 alias opDollar = length; 100 } 101 102 struct ItemsAdded(T) 103 { 104 alias ItemsAddedRange = ReadOnlyList!T.Range; 105 106 private size_t myNewItemsFirstIndex; 107 private ItemsAddedRange myNewItems; 108 109 @property size_t newItemsFirstIndex() const { return myNewItemsFirstIndex; } 110 @property ItemsAddedRange newItems() { return myNewItems; } 111 112 this(size_t newItemsFirstIndex, ItemsAddedRange newItems) 113 { 114 this.myNewItemsFirstIndex = newItemsFirstIndex; 115 this.myNewItems = newItems; 116 } 117 } 118 119 struct ItemsRemoved(T) 120 { 121 private bool myIsReset; 122 private ReadOnlyList!T myOldItems; 123 124 @property bool isReset() const { return myIsReset; } 125 @property ReadOnlyList!T oldItems() { return myOldItems; } 126 127 this(bool isReset, ReadOnlyList!T oldItems) 128 { 129 this.myIsReset = isReset; 130 this.myOldItems = oldItems; 131 } 132 } 133 134 abstract class ReadOnlyReactiveList(T) : ReadOnlyList!T 135 { 136 alias ItemsAddedDesc = ItemsAdded!T; 137 alias ItemsRemovedDesc = ItemsRemoved!T; 138 139 @property Observable!ItemsAddedDesc itemsAdded() { return itemsAddedSubject; } 140 private Subject!ItemsAddedDesc itemsAddedSubject; 141 142 // This property do not guarantee that remove is complete on reset 143 @property Observable!ItemsRemovedDesc itemsRemoved() { return itemsRemovedSubject; } 144 private Subject!ItemsRemovedDesc itemsRemovedSubject; 145 146 this() 147 { 148 itemsAddedSubject = new Subject!ItemsAddedDesc; 149 itemsRemovedSubject = new Subject!ItemsRemovedDesc; 150 } 151 } 152 153 class ReactiveList(T) : ReadOnlyReactiveList!T 154 { 155 void add(T item) 156 { 157 auto i = impl.length; 158 impl.insertBack(item); 159 auto listChange = ItemsAdded!T(i, this[i..$]); 160 itemsAddedSubject.next(listChange); 161 } 162 163 // covers T[] 164 // covers Array!T 165 void addRange(Stuff)(Stuff stuff) if (isInputRange!Stuff && isImplicitlyConvertible!(ElementType!Stuff, T)) 166 { 167 auto i = impl.length; 168 impl.insertBack(stuff); 169 170 if(!itemsAddedSubject.hasSubscribers) return; 171 auto listChange = ItemsAdded!T(i, this[i..$]); 172 itemsAddedSubject.next(listChange); 173 } 174 175 void remove(T v) 176 { 177 bool found = false; 178 for(size_t i = 0; i<impl.length; i++) 179 { 180 if(impl[i] == v) 181 { 182 impl.linearRemove(impl[i..i+1]); 183 found = true; 184 break; 185 } 186 } 187 188 if(!found) throw new Error("Item not found"); 189 190 if(!itemsRemovedSubject.hasSubscribers) return; 191 auto listChange = ItemsRemoved!T(false, new ReadOnlyList!T(v)); 192 itemsRemovedSubject.next(listChange); 193 } 194 195 void removeRange(Stuff)(Stuff val) if (isInputRange!Stuff && isImplicitlyConvertible!(ElementType!Stuff, T)) 196 { 197 static if(is(Stuff == ReadOnlyList!T)) 198 { 199 assert(val !is this); 200 } else { 201 auto oldItems = new ReactiveList!T; 202 } 203 204 foreach(v; val) 205 { 206 bool found = false; 207 for(size_t i = 0; i<impl.length; i++) 208 { 209 if(impl[i] == v) 210 { 211 impl.linearRemove(impl[i..i+1]); 212 found = true; 213 static if(!is(Stuff == ReadOnlyList!T)) 214 { 215 if(itemsRemovedSubject.hasSubscribers) 216 { 217 if(oldItems.length > 0) oldItems[0] = v; 218 else oldItems.add(v); 219 auto listChange = ItemsRemoved!T(true, oldItems); 220 itemsRemovedSubject.next(listChange); 221 } 222 } 223 break; 224 } 225 } 226 227 if(!found) throw new Error("Item not found"); 228 } 229 230 static if(is(Stuff == ReadOnlyList!T)) 231 { 232 if(itemsRemovedSubject.hasSubscribers) 233 { 234 auto listChange = ItemsRemoved!T(true, val); 235 itemsRemovedSubject.next(listChange); 236 } 237 } 238 } 239 240 void clear() 241 { 242 if(!length) return; 243 auto listChange = ItemsRemoved!T(true, this); 244 itemsRemovedSubject.next(listChange); 245 246 impl.clear(); 247 } 248 249 void set(size_t index, T value) 250 { 251 T oldValue = impl[index]; 252 impl[index] = value; 253 254 auto listChange = ItemsRemoved!T(false, new ReadOnlyList!T(oldValue)); 255 itemsRemovedSubject.next(listChange); 256 257 auto listChange2 = ItemsAdded!T(index, this[index..index+1]); 258 itemsAddedSubject.next(listChange2); 259 } 260 261 T opIndexAssign(T v, size_t i) 262 { 263 set(i, v); 264 return v; 265 } 266 } 267 268 unittest 269 { 270 import modernui.rx; 271 272 auto list = new ReactiveList!int; 273 assert(list.length == 0); 274 275 list.addRange([5]); 276 assert(list.length == 1); 277 assert(list[0] == 5); 278 279 list.set(0, 6); 280 assert(list[0] == 6); 281 assert(list.length == 1); 282 283 list.removeRange([6]); 284 assert(list.length == 0); 285 286 list.add(1); 287 assert(list[0] == 1); 288 assert(list.length == 1); 289 foreach(i; list[]) 290 { 291 assert(i == 1); 292 } 293 294 list.add(7); 295 list.add(9); 296 assert(list[2] == 9); 297 assert(list.contains(7)); 298 assert(!list.contains(700)); 299 list[2] = 10; 300 assert(list[2] == 10); 301 assert(list.length == 3); 302 list.remove(7); 303 assert(list.length == 2); 304 305 list.clear(); 306 assert(list.length == 0); 307 308 bool changeInvoked = false; 309 list.itemsAdded.then!(ItemsAdded!int)((a) { 310 changeInvoked = true; 311 }); 312 list.add(9); 313 assert(changeInvoked); 314 }