1 /**
2  * Widget base class and the geometry primitives it works with.
3  *
4  * A `Widget` owns a single native window handle (`HWND`) and the common
5  * operations over it: visibility, bounds, enablement, focus and deterministic
6  * teardown. `Window` (top-level windows) and `Control` (common controls) both
7  * derive from it.
8  *
9  * Handle lifetime: a widget owns its `HWND`. Because D's garbage collector is
10  * non-deterministic, call `dispose()` for prompt, predictable cleanup — it
11  * destroys the native window and unregisters the widget. While a widget's HWND
12  * is alive the widget is pinned as a GC root so it cannot be collected out from
13  * under the message loop.
14  */
15 module deft.widget;
16 
17 version (Windows):
18 
19 import core.memory : GC;
20 import core.sys.windows.windows;
21 
22 import deft.platform.win32.wndproc : registerWidget, unregisterWidget;
23 
24 /// An axis-aligned rectangle in device pixels.
25 struct Rect
26 {
27 	int x;
28 	int y;
29 	int width;
30 	int height;
31 
32 	/// Build a `Rect` from a Win32 `RECT` (left/top/right/bottom).
33 	static Rect fromRECT(RECT r) @safe pure nothrow @nogc
34 	{
35 		return Rect(r.left, r.top, r.right - r.left, r.bottom - r.top);
36 	}
37 
38 	/// Convert to a Win32 `RECT`.
39 	RECT toRECT() const @safe pure nothrow @nogc
40 	{
41 		RECT r;
42 		r.left = x;
43 		r.top = y;
44 		r.right = x + width;
45 		r.bottom = y + height;
46 		return r;
47 	}
48 }
49 
50 /// A width/height pair in device pixels.
51 struct Size
52 {
53 	int width;
54 	int height;
55 }
56 
57 /// Per-edge spacing in device pixels.
58 struct Padding
59 {
60 	int left;
61 	int top;
62 	int right;
63 	int bottom;
64 
65 	/// Equal padding on every edge.
66 	static Padding all(int n) @safe pure nothrow @nogc
67 	{
68 		return Padding(n, n, n, n);
69 	}
70 
71 	/// `h` on the left and right edges, `v` on the top and bottom.
72 	static Padding symmetric(int h, int v) @safe pure nothrow @nogc
73 	{
74 		return Padding(h, v, h, v);
75 	}
76 }
77 
78 /**
79  * Abstract base for everything that owns a native window.
80  */
81 abstract class Widget
82 {
83 	/// The native window handle this widget owns (null until created).
84 	protected HWND handle_;
85 
86 	/// The widget this one is parented to, if any.
87 	protected Widget parent_;
88 
89 	/// Child widgets, in z/insertion order.
90 	protected Widget[] children_;
91 
92 	/// Cached visibility flag.
93 	protected bool visible_ = true;
94 
95 	/// Cached bounds (window-relative for children, screen-relative for windows).
96 	protected Rect bounds_;
97 
98 	private bool disposed_;
99 
100 	/// Whether this widget is currently pinned as a GC root (see `registerHandle`).
101 	private bool rooted_;
102 
103 	/**
104 	 * The native window handle this widget owns (null until created).
105 	 *
106 	 * Read-only: a widget owns its handle for its whole lifetime and the framework
107 	 * relies on that invariant (GC pinning, the HWND→widget registry). Consumers
108 	 * may read it for interop with raw Win32 calls but cannot reassign it.
109 	 */
110 	final HWND handle() @property @safe nothrow @nogc
111 	{
112 		return handle_;
113 	}
114 
115 	/// The widget this one is parented to, if any. Read-only; see `addChild`.
116 	final Widget parent() @property @safe nothrow @nogc
117 	{
118 		return parent_;
119 	}
120 
121 	/// The child widgets, in z/insertion order. Read-only; see `addChild`.
122 	final Widget[] children() @property @safe nothrow @nogc
123 	{
124 		return children_;
125 	}
126 
127 	/// Whether the widget is currently visible. Read-only; see `setVisible`.
128 	final bool visible() @property @safe nothrow @nogc
129 	{
130 		return visible_;
131 	}
132 
133 	/// The widget's last-known bounds. Read-only; see `setBounds`/`getBounds`.
134 	final Rect bounds() @property @safe nothrow @nogc
135 	{
136 		return bounds_;
137 	}
138 
139 	/// Raw handle accessor for subclasses and the backend.
140 	protected HWND rawHandle() @property @safe nothrow @nogc
141 	{
142 		return handle_;
143 	}
144 
145 	/// Make the widget visible.
146 	void show()
147 	{
148 		setVisible(true);
149 	}
150 
151 	/// Hide the widget.
152 	void hide()
153 	{
154 		setVisible(false);
155 	}
156 
157 	/// Set visibility, updating the native window if it exists.
158 	void setVisible(bool value)
159 	{
160 		visible_ = value;
161 		if (handle)
162 			ShowWindow(handle, value ? SW_SHOW : SW_HIDE);
163 	}
164 
165 	/// Move/resize the widget, updating the native window if it exists.
166 	void setBounds(Rect r)
167 	{
168 		bounds_ = r;
169 		if (handle)
170 			MoveWindow(handle, r.x, r.y, r.width, r.height, TRUE);
171 	}
172 
173 	/// The widget's last-known bounds.
174 	Rect getBounds()
175 	{
176 		return bounds_;
177 	}
178 
179 	/// The widget's client area (origin at 0,0). Empty if not yet created.
180 	Rect getClientRect()
181 	{
182 		if (!handle)
183 			return Rect.init;
184 		RECT rc;
185 		GetClientRect(handle, &rc);
186 		return Rect.fromRECT(rc);
187 	}
188 
189 	/// Enable or disable input for the widget.
190 	void setEnabled(bool enabled)
191 	{
192 		if (handle)
193 			EnableWindow(handle, enabled ? TRUE : FALSE);
194 	}
195 
196 	/// Whether the widget currently accepts input.
197 	bool isEnabled()
198 	{
199 		if (!handle)
200 			return false;
201 		return IsWindowEnabled(handle) != FALSE;
202 	}
203 
204 	/// Give the widget keyboard focus.
205 	void setFocus()
206 	{
207 		if (handle)
208 			SetFocus(handle);
209 	}
210 
211 	/// Request a repaint of the whole widget.
212 	void invalidate()
213 	{
214 		if (handle)
215 			InvalidateRect(handle, null, TRUE);
216 	}
217 
218 	/**
219 	 * The widget's preferred size, used by the layout engine for non-stretching
220 	 * (proportion 0) items. The base widget has no intrinsic size; controls
221 	 * override this.
222 	 */
223 	Size getPreferredSize()
224 	{
225 		return Size.init;
226 	}
227 
228 	/// Append a child and set its parent to this widget.
229 	void addChild(Widget child)
230 	{
231 		if (child is null)
232 			return;
233 		children_ ~= child;
234 		child.parent_ = this;
235 	}
236 
237 	/// Remove a child and clear its parent link. Unknown children are ignored.
238 	void removeChild(Widget child)
239 	{
240 		foreach (i, c; children)
241 		{
242 			if (c is child)
243 			{
244 				children_ = children_[0 .. i] ~ children_[i + 1 .. $];
245 				if (child !is null)
246 					child.parent_ = null;
247 				return;
248 			}
249 		}
250 	}
251 
252 	/**
253 	 * Deterministically tear the widget down: dispose children, detach from the
254 	 * parent, destroy the native window, unregister it, and release the GC root.
255 	 * Idempotent — safe to call more than once.
256 	 */
257 	void dispose()
258 	{
259 		if (disposed_)
260 			return;
261 		disposed_ = true;
262 
263 		foreach (child; children_.dup)
264 			child.dispose();
265 		children_ = null;
266 
267 		if (parent_ !is null)
268 			parent_.removeChild(this);
269 
270 		// Destroying the native window delivers WM_NCDESTROY synchronously, where
271 		// `releaseHandle` unregisters it and drops the GC root. The trailing call
272 		// covers the rare case of a widget that never owned a handle.
273 		if (handle_ !is null)
274 			DestroyWindow(handle_);
275 		releaseHandle();
276 	}
277 
278 	/**
279 	 * Associate this widget's freshly-created HWND with the dispatch machinery
280 	 * and pin it as a GC root. Call once, immediately after `handle` is set.
281 	 */
282 	protected void registerHandle()
283 	{
284 		if (!handle)
285 			return;
286 		GC.addRoot(cast(void*) this);
287 		rooted_ = true;
288 		SetWindowLongPtrW(handle, GWLP_USERDATA, cast(LONG_PTR) cast(void*) this);
289 		registerWidget(handle, this);
290 	}
291 
292 	/**
293 	 * Release the framework's hold on the native window: drop it from the
294 	 * HWND→Widget registry, clear its back-pointer and release the GC root.
295 	 *
296 	 * Driven by `WM_NCDESTROY` so it runs no matter who destroyed the window —
297 	 * including a user closing it, which never reaches `dispose()`. Idempotent.
298 	 */
299 	private void releaseHandle()
300 	{
301 		if (handle_ !is null)
302 		{
303 			unregisterWidget(handle_);
304 			SetWindowLongPtrW(handle_, GWLP_USERDATA, 0);
305 			handle_ = null;
306 		}
307 		if (rooted_)
308 		{
309 			GC.removeRoot(cast(void*) this);
310 			rooted_ = false;
311 		}
312 	}
313 
314 	/**
315 	 * Handle a window message routed from the master window procedure.
316 	 *
317 	 * The default implementation defers to `DefWindowProcW`. Subclasses override
318 	 * to handle specific messages and call `super.processMessage` for the rest.
319 	 */
320 	LRESULT processMessage(UINT msg, WPARAM wParam, LPARAM lParam)
321 	{
322 		if (msg == WM_NCDESTROY)
323 		{
324 			// The last message a window receives. Release our hold here so a
325 			// user-closed window (which never calls dispose()) is unregistered
326 			// and unrooted instead of leaking with a stale registry entry.
327 			HWND h = handle_;
328 			releaseHandle();
329 			return DefWindowProcW(h, msg, wParam, lParam);
330 		}
331 		return DefWindowProcW(handle, msg, wParam, lParam);
332 	}
333 }
334 
335 /**
336  * The handle of the first focusable control among `kids`, searched depth-first.
337  *
338  * A control is focusable when it is a tab stop and currently visible and
339  * enabled. Non-tab-stop containers (such as a `Panel`) are descended into, so a
340  * control nested inside one is still found — without this, initial focus could
341  * land on the bare window and leave keyboard and screen-reader users stranded.
342  */
343 HWND firstFocusableIn(Widget[] kids) nothrow
344 {
345 	foreach (child; kids)
346 	{
347 		if (child is null || child.handle is null
348 			|| !IsWindowVisible(child.handle)
349 			|| !IsWindowEnabled(child.handle))
350 			continue;
351 		auto style = GetWindowLongW(child.handle, GWL_STYLE);
352 		if (style & WS_TABSTOP)
353 			return child.handle;
354 		if (auto found = firstFocusableIn(child.children))
355 			return found;
356 	}
357 	return null;
358 }
359 
360 unittest
361 {
362 	// Rect <-> RECT conversion: a RECT is left/top/right/bottom; a Rect is
363 	// x/y/width/height. fromRECT computes the extents and toRECT inverts it.
364 	RECT r;
365 	r.left = 10;
366 	r.top = 20;
367 	r.right = 110;
368 	r.bottom = 70;
369 
370 	auto rect = Rect.fromRECT(r);
371 	assert(rect == Rect(10, 20, 100, 50));
372 
373 	auto back = rect.toRECT();
374 	assert(back.left == 10 && back.top == 20 && back.right == 110 && back.bottom == 70);
375 
376 	// Round-trips for any rectangle.
377 	auto rt = Rect(3, 7, 40, 9);
378 	auto rtBack = Rect.fromRECT(rt.toRECT());
379 	assert(rtBack == rt);
380 }
381 
382 unittest
383 {
384 	// Padding factory helpers.
385 	assert(Padding.all(5) == Padding(5, 5, 5, 5));
386 	assert(Padding.symmetric(8, 4) == Padding(8, 4, 8, 4));
387 	assert(Padding.all(0) == Padding.init);
388 }