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 }