1 module fluid.future.context;
2 
3 import std.meta;
4 import std.traits;
5 
6 import fluid.types;
7 import fluid.tree : TreeAction;
8 import fluid.future.stack;
9 import fluid.future.static_id;
10 
11 @safe:
12 
13 struct TreeContext {
14 
15     TreeContextData* ptr;
16 
17     alias ptr this;
18 
19     /// Create the context if it doesn't already exist.
20     void prepare() {
21 
22         if (ptr is null) {
23             ptr = new TreeContextData();
24         }
25 
26     }
27 
28     bool opCast(T : bool)() const {
29 
30         return ptr !is null;
31 
32     }
33 
34 }
35 
36 struct TreeContextData {
37 
38     public {
39 
40         /// Keeps track of currently active I/O systems.
41         TreeIOContext io;
42 
43         /// Manages and runs tree actions.
44         TreeActionContext actions;
45 
46     }
47 
48     private {
49         int _lockTint;
50         auto _tint = Color(0xff, 0xff, 0xff, 0xff);
51     }
52 
53     /// Tint is a transitive styling property that can be used to reduce color intensity of everything
54     /// that a node draws. Tint applies per channel, which means it can be used to reduce opacity (by changing
55     /// the alpha channel) and any of the three RGB colors.
56     ///
57     /// A tint of value `0` sets intensity to 0% (disable). A tint of value `255` sets intensity to 100% (no change).
58     ///
59     /// See_Also:
60     ///     `Style.tint`
61     /// Returns: The current tint.
62     Color tint() const nothrow {
63         return _tint;
64     }
65 
66     package (fluid)
67     Color tint(Color newValue) nothrow {
68         if (_lockTint > 0) {
69             return _tint;
70         }
71         else {
72             return _tint = newValue;
73         }
74     }
75 
76     /// Lock tint in place, preventing it from changing, or cancel a lock, making changes possible again.
77     ///
78     /// This function is needed for compatibility with the legacy `FluidBackend` system.
79     /// Locks can be stacked, so if `lockTint()` is called twice, `unlockTint()` also has to be called twice
80     /// to unlock tinting.
81     ///
82     /// It is expected that this function will be deprecated as soon as `FluidBackend` is no longer a part
83     /// of Fluid. It will then be deleted in the next minor release.
84     void lockTint() {
85         _lockTint++;
86     }
87 
88     /// ditto
89     void unlockTint() {
90         if (_lockTint > 0) {
91             _lockTint--;
92         }
93     }
94 
95 }
96 
97 /// Active context for I/O operations. Keeps track of currently active systems for each I/O interface.
98 ///
99 /// I/O systems are changed by a replace operation. `replace` takes the new I/O systems, but returns the one set
100 /// previously. This can be used to manage I/Os as a stack:
101 ///
102 /// ---
103 /// auto previous = io.replace(id, this);
104 /// scope (exit) io.replace(id, previous);
105 /// ---
106 struct TreeIOContext {
107 
108     import std.range;
109     import std.algorithm : completeSort;
110 
111     struct IOInstance {
112         IOID id;
113         IO io;
114         int opCmp(const IOInstance rhs) const {
115             return id.opCmp(rhs.id);
116         }
117         int opCmp(const IOID rhs) const {
118             return id.opCmp(rhs);
119         }
120     }
121 
122     /// Key-value pairs of active I/O systems. Each pair contains the system and the ID of the interface
123     /// it implements. Pairs are sorted by the interface ID.
124     private SortedRange!(IOInstance[]) activeIOs;
125 
126     /// Returns:
127     ///     The active instance of the given IO interface.
128     ///     `null`, no instance of this interface is currently active.
129     /// Params:
130     ///     id = ID of the IO interface to load.
131     IO get(IOID id) {
132 
133         auto range = activeIOs.equalRange(id);
134         if (range.empty) {
135             return null;
136         }
137         else {
138             return range.front.io;
139         }
140 
141     }
142 
143     /// ditto
144     T get(T)() {
145 
146         const id = ioID!T;
147         return cast(T) get(id);
148 
149     }
150 
151     /// Set currently active I/O instance for a set interface.
152     /// Params:
153     ///     id     = ID of the IO interface the instance implements.
154     ///     system = System to activate.
155     /// Returns:
156     ///     Returns *previously set* I/O instance.
157     IO replace(IOID id, IO system) {
158 
159         auto range = activeIOs.equalRange(id);
160         auto instance = IOInstance(id, system);
161 
162         // Nothing set, add a new value
163         if (range.empty) {
164             IOInstance[1] instanceRange = instance;
165             completeSort(activeIOs, instanceRange[]);
166             activeIOs = assumeSorted(activeIOs.release ~ instanceRange[]);
167             return null;
168         }
169 
170         // Override the previous result
171         else {
172             auto previous = range.front;
173             range.front = instance;
174             return previous.io;
175         }
176 
177     }
178 
179     /// Iterate on all active I/O systems.
180     ///
181     /// Elements are passed by value and cannot be modified.
182     ///
183     /// Returns:
184     ///     A sorted input range of `(IOID id, IO io)` pairs.
185     auto opIndex() {
186 
187         // `map` should prevent modifications
188         import std.algorithm : map;
189 
190         return activeIOs.save.map!(a => a).assumeSorted;
191 
192     }
193 
194     /// Create a copy of the context.
195     ///
196     /// This creates a shallow clone: I/O systems can be replaced in the copy without affecting the original
197     /// and vice-versa. The individual systems will *not* be copied.
198     ///
199     /// This is useful if the I/O stack created at some specific point in time has to be reproduced elsewhere.
200     /// For example, in a drag-and-drop scenario, while a node is being dragged, it does not have a place as a child
201     /// of any node. A copy of the I/O stack it had at the start is used to continue drawing while the node
202     /// is "in air".
203     ///
204     /// Returns:
205     ///     A shallow copy of the I/O context.
206     TreeIOContext dup() {
207         return TreeIOContext(activeIOs.release.dup.assumeSorted);
208     }
209 
210 }
211 
212 enum isIO(T) = is(T == interface)
213     && is(T : IO)
214     && !is(T == IO);
215 
216 /// Get all IOs implemented by the given type
217 alias allIOs(T) = Filter!(isIO, InterfacesTuple!T);
218 
219 interface HasContext {
220 
221     /// Returns: The current tree context.
222     inout(TreeContext) treeContext() inout nothrow;
223 
224 }
225 
226 interface IO : HasContext {
227 
228     bool opEquals(const Object) const;
229 
230     /// Load a resource by reference. This is the same as `Node.load`.
231     /// Params:
232     ///     resource = Resource to load. It will be updated with identifying information.
233     void loadTo(this This, T)(ref T resource) {
234 
235         auto io = cast(This) this;
236 
237         // Load the resource
238         const id = io.load(resource);
239 
240         // Pass data into the resource
241         resource.load(io, id);
242 
243     }
244 
245 }
246 
247 IOID ioID(T)()
248 if (isIO!T) {
249 
250     return IOID(staticID!T);
251 
252 }
253 
254 /// ID for an I/O interface.
255 struct IOID {
256 
257     StaticID id;
258 
259     int opCmp(const IOID rhs) const {
260         return id.opCmp(rhs.id);
261     }
262 
263 }
264 
265 /// Keeps track of currently active actions.
266 struct TreeActionContext {
267 
268     import std.array;
269 
270     private {
271 
272         struct RunningAction {
273 
274             TreeAction action;
275             int generation;
276 
277             bool isStopped() const {
278                 return action.generation > generation;
279             }
280 
281         }
282 
283         /// Currently running actions.
284         Appender!(RunningAction[]) _actions;
285 
286         /// Number of running iterators. Removing tree actions will only happen if there is exactly one
287         /// running iterator, as to not break the other ones.
288         ///
289         /// Multiple iterators may run in case a tree action draws nodes on its own: one iterator triggers
290         /// the action, and the drawn node activates another iterator.
291         int _runningIterators;
292 
293     }
294 
295     /// Start a number of tree actions. As the node tree is drawn, the action's hook will be called whenever
296     /// a relevant place is reached in the tree.
297     ///
298     /// To stop a running action, call the action's `stop` method. Most tree actions will do it automatically
299     /// as soon as their job is finished.
300     ///
301     /// If the action is already running, the previous run will be aborted. The action can only run once at a time.
302     ///
303     /// Params:
304     ///     actions = Actions to spawn.
305     void spawn(TreeAction[] actions...) {
306 
307         _actions.reserve(_actions[].length + actions.length);
308 
309         // Start every action and run the hook
310         foreach (action; actions) {
311 
312             _actions ~= RunningAction(action, ++action.generation);
313             action.started();
314 
315 
316         }
317 
318     }
319 
320     /// List all currently active actions in a loop.
321     int opApply(int delegate(TreeAction) @safe yield) {
322 
323         // Update the iterator counter
324         _runningIterators++;
325         scope (exit) _runningIterators--;
326 
327         bool kept;
328 
329         // Iterate on active actions
330         // Do *not* increment if an action was removed
331         for (size_t i = 0; i < _actions[].length; i += kept) {
332 
333             auto action = _actions[][i];
334             kept = true;
335 
336             // If there's one running iterator, remove it from the array
337             // Don't pass stopped actions to the iterator
338             if (action.isStopped) {
339 
340                 if (_runningIterators == 1) {
341                     _actions[][i] = _actions[][$-1];
342                     _actions.shrinkTo(_actions[].length - 1);
343                     kept = false;
344                 }
345                 continue;
346 
347             }
348 
349             // Run the hook
350             if (auto result = yield(action.action)) return result;
351 
352         }
353 
354         return 0;
355 
356     }
357 
358 }