1 /**
2  * Periodic and one-shot timers.
3  *
4  * `Timer` wraps the Win32 `SetTimer`/`KillTimer` pair. A timer is owned by a
5  * `Widget` (whose window receives the `WM_TIMER` messages) and fires its
6  * `onTick` event on each tick. The master window procedure routes `WM_TIMER`
7  * here by timer id.
8  *
9  * Timer ids are small integers from a process-wide counter, never object
10  * pointers — D's garbage collector may relocate an object, which would
11  * invalidate a pointer used as an id and misroute ticks.
12  */
13 module deft.controls.timer;
14 
15 version (Windows):
16 
17 import core.sys.windows.windows;
18 
19 import deft.events;
20 import deft.widget : Widget;
21 
22 private __gshared Timer[uint] g_timers;
23 private __gshared uint g_nextTimerId = 1;
24 
25 /**
26  * Dispatch a `WM_TIMER` to its `Timer`'s `onTick`.
27  *
28  * Returns `true` if a live timer with the given id was found. A one-shot timer
29  * stops itself after firing.
30  */
31 bool dispatchTimer(uint id)
32 {
33 	if (auto timer = id in g_timers)
34 	{
35 		auto t = *timer;
36 		t.onTick.fire();
37 		if (t.oneShot_)
38 			t.stop();
39 		return true;
40 	}
41 	return false;
42 }
43 
44 /**
45  * Stop and forget every timer owned by `owner`.
46  *
47  * Called when the owner's window is destroyed: Win32 kills the native timers
48  * along with the `HWND`, but Deft's registry entries and `running_` flags would
49  * otherwise survive — leaking the timer objects and making `isRunning()` lie.
50  */
51 void stopTimersFor(Widget owner)
52 {
53 	uint[] dead;
54 	foreach (id, t; g_timers)
55 		if (t.owner_ is owner)
56 			dead ~= id;
57 	foreach (id; dead)
58 	{
59 		g_timers[id].running_ = false;
60 		g_timers.remove(id);
61 	}
62 }
63 
64 /// A repeating or one-shot timer bound to an owner widget's window.
65 class Timer
66 {
67 	private Widget owner_;
68 	private uint id_;
69 	private bool running_;
70 	private bool oneShot_;
71 
72 	/// Fired on every tick.
73 	Event!() onTick;
74 
75 	/**
76 	 * Create a timer owned by `owner`.
77 	 *
78 	 * The owner's window receives the underlying `WM_TIMER` messages, so the
79 	 * owner must have a live handle while the timer runs.
80 	 */
81 	this(Widget owner)
82 	{
83 		owner_ = owner;
84 		id_ = g_nextTimerId++;
85 	}
86 
87 	/// Whether the timer is currently running.
88 	bool isRunning() const @safe pure nothrow @nogc
89 	{
90 		return running_;
91 	}
92 
93 	/**
94 	 * Start (or restart) the timer.
95 	 *
96 	 * Params:
97 	 *   intervalMs = tick interval in milliseconds.
98 	 *   oneShot    = when true, the timer stops itself after the first tick.
99 	 */
100 	void start(int intervalMs, bool oneShot = false)
101 	{
102 		if (owner_ is null || owner_.handle is null)
103 			return;
104 		oneShot_ = oneShot;
105 		g_timers[id_] = this;
106 		SetTimer(owner_.handle, id_, cast(UINT) intervalMs, null);
107 		running_ = true;
108 	}
109 
110 	/// Stop the timer. Safe to call when not running.
111 	void stop()
112 	{
113 		if (!running_)
114 			return;
115 		if (owner_ !is null && owner_.handle !is null)
116 			KillTimer(owner_.handle, id_);
117 		g_timers.remove(id_);
118 		running_ = false;
119 	}
120 }