The OpenD Programming Language

1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
2 
3 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay
4 
5 // me@arsd:~/.kde/share/config$ vim kdeglobals
6 
7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too.
8 
9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/
10 
11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever.
12 
13 // responsive minigui, menu search, and file open with a preview hook on the side.
14 
15 // FIXME: add menu checkbox and menu icon eventually
16 
17 /*
18 
19 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan
20 
21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it...
22 */
23 
24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places
25 
26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk.
27 
28 // FIXME: opt-in file picker widget with image support
29 
30 // FIXME: number widget
31 
32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm
33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles
34 
35 // osx style menu search.
36 
37 // would be cool for a scroll bar to have marking capabilities
38 // kinda like vim's marks just on clicks etc and visual representation
39 // generically. may be cool to add an up arrow to the bottom too
40 //
41 // leave a shadow of where you last were for going back easily
42 
43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various
44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for
45 // the window.
46 
47 // so what about context menus?
48 
49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
50 
51 // FIXME: make the scroll thing go to bottom when the content changes.
52 
53 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image
54 
55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
56 
57 
58 // FIXME: add a command search thingy built in and implement tip.
59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!
60 
61 // On Windows:
62 // FIXME: various labels look broken in high contrast mode
63 // FIXME: changing themes while the program is upen doesn't trigger a redraw
64 
65 // add note about manifest to documentation. also icons.
66 
67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar
68 // FIXME: clear the corner of scrollbars if they pop up
69 
70 // minigui needs to have a stdout redirection for gui mode on windows writeln
71 
72 // I kinda wanna do state reacting. sort of. idk tho
73 
74 // need a viewer widget that works like a web page - arrows scroll down consistently
75 
76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside.
77 
78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two.
79 // and help info about menu items.
80 // and search in menus?
81 
82 // FIXME: a scroll area event signaling when a thing comes into view might be good
83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must
84 
85 // FIXME: unify Windows style line endings
86 
87 /*
88 	TODO:
89 
90 	pie menu
91 
92 	class Form with submit behavior -- see AutomaticDialog
93 
94 	disabled widgets and menu items
95 
96 	event cleanup
97 	tooltips.
98 	api improvements
99 
100 	margins are kinda broken, they don't collapse like they should. at least.
101 
102 	a table form btw would be a horizontal layout of vertical layouts holding each column
103 	that would give the same width things
104 */
105 
106 /*
107 
108 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets
109 */
110 
111 /++
112 	minigui is a smallish GUI widget library, aiming to be on par with at least
113 	HTML4 forms and a few other expected gui components. It uses native controls
114 	on Windows and does its own thing on Linux (Mac is not currently supported but
115 	may be later, and should use native controls) to keep size down. The Linux
116 	appearance is similar to Windows 95 and avoids using images to maintain network
117 	efficiency on remote X connections, though you can customize that.
118 
119 
120 	minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color],
121 	on which it is built. simpledisplay provides the low-level interfaces and minigui
122 	builds the concept of widgets inside the windows on top of it.
123 
124 	Its #1 goal is to be useful without being large and complicated like GTK and Qt.
125 	It isn't hugely concerned with appearance - on Windows, it just uses the native
126 	controls and native theme, and on Linux, it keeps it simple and I may change that
127 	at any time, though after May 2021, you can customize some things with css-inspired
128 	[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
129 	you can use the custom implementation there too, but... you shouldn't.)
130 
131 	The event model is similar to what you use in the browser with Javascript and the
132 	layout engine tries to automatically fit things in, similar to a css flexbox.
133 
134 	FOR BEST RESULTS: be sure to link with the appropriate subsystem command
135 	`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
136 	of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
137 
138 	Otherwise you'll get a console and possibly other visual bugs. But if you do use
139 	the subsystem:windows, note that Phobos' writeln will crash the program!
140 
141 	HTML_To_Classes:
142 	$(SMALL_TABLE
143 		HTML Code | Minigui Class
144 
145 		`<input type="text">` | [LineEdit]
146 		`<textarea>` | [TextEdit]
147 		`<select>` | [DropDownSelection]
148 		`<input type="checkbox">` | [Checkbox]
149 		`<input type="radio">` | [Radiobox]
150 		`<button>` | [Button]
151 	)
152 
153 
154 	Stretchiness:
155 		The default is 4. You can use larger numbers for things that should
156 		consume a lot of space, and lower numbers for ones that are better at
157 		smaller sizes.
158 
159 	Overlapped_input:
160 		COMING EVENTUALLY:
161 		minigui will include a little bit of I/O functionality that just works
162 		with the event loop. If you want to get fancy, I suggest spinning up
163 		another thread and posting events back and forth.
164 
165 	$(H2 Add ons)
166 		See the `minigui_addons` directory in the arsd repo for some add on widgets
167 		you can import separately too.
168 
169 	$(H3 XML definitions)
170 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
171 
172 	$(H3 Scriptability)
173 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
174 		in this documentation, it means you can call it from the script language.
175 
176 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
177 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
178 
179 		---
180 		import arsd.minigui_xml;
181 		import arsd.script;
182 
183 		var globals = var.emptyObject;
184 		globals.makeWidgetFromString = &makeWidgetFromString;
185 
186 		// this now works
187 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
188 		---
189 
190 		More to come.
191 
192 	History:
193 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
194 
195 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
196 		tag this as version 2.0.
197 
198 		Among the changes:
199 		$(LIST
200 			* The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard.
201 
202 			See [Event] for details.
203 
204 			* A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead.
205 
206 			See [DoubleClickEvent] for details.
207 
208 			* Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows.
209 
210 			See [Widget.Style] for details.
211 
212 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
213 
214 			* Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it.
215 
216 			* Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst.
217 
218 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
219 
220 			* Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does.
221 
222 			* Various non-breaking additions.
223 		)
224 +/
225 module arsd.minigui;
226 
227 /++
228 	This hello world sample will have an oversized button, but that's ok, you see your first window!
229 +/
230 version(Demo)
231 unittest {
232 	import arsd.minigui;
233 
234 	void main() {
235 		auto window = new MainWindow();
236 
237 		// note the parent widget is almost always passed as the last argument to a constructor
238 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
239 		auto button = new Button("Close", window);
240 		button.addWhenTriggered({
241 			window.close();
242 		});
243 
244 		window.loop();
245 	}
246 
247 	main(); // exclude from docs
248 }
249 
250 /++
251 	This example shows one way you can partition your window into a header
252 	and sidebar. Here, the header and sidebar have a fixed width, while the
253 	rest of the content sizes with the window.
254 
255 	It might be a new way of thinking about window layout to do things this
256 	way - perhaps [GridLayout] more matches your style of thought - but the
257 	concept here is to partition the window into sub-boxes with a particular
258 	size, then partition those boxes into further boxes.
259 
260 	$(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.)
261 
262 	So to make the header, start with a child layout that has a max height.
263 	It will use that space from the top, then the remaining children will
264 	split the remaining area, meaning you can think of is as just being another
265 	box you can split again. Keep splitting until you have the look you desire.
266 +/
267 // https://github.com/adamdruppe/arsd/issues/310
268 version(minigui_screenshots)
269 @Screenshot("layout")
270 unittest {
271 	import arsd.minigui;
272 
273 	// This helper class is just to help make the layout boxes visible.
274 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
275 	class ColorWidget : Widget {
276 		this(Color color, Widget parent) {
277 			this.color = color;
278 			super(parent);
279 		}
280 		Color color;
281 		class Style : Widget.Style {
282 			override WidgetBackground background() { return WidgetBackground(color); }
283 		}
284 		mixin OverrideStyle!Style;
285 	}
286 
287 	void main() {
288 		auto window = new Window;
289 
290 		// the key is to give it a max height. This is one way to do it:
291 		auto header = new class HorizontalLayout {
292 			this() { super(window); }
293 			override int maxHeight() { return 50; }
294 		};
295 		// this next line is a shortcut way of doing it too, but it only works
296 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
297 		// is good to know how to make a new class like above anyway.
298 		// auto header = new HorizontalLayout(50, window);
299 
300 		auto bar = new HorizontalLayout(window);
301 
302 		// or since this is so common, VerticalLayout and HorizontalLayout both
303 		// can just take an argument in their constructor for max width/height respectively
304 
305 		// (could have tone this above too, but I wanted to demo both techniques)
306 		auto left = new VerticalLayout(100, bar);
307 
308 		// and this is the main section's container. A plain Widget instance is good enough here.
309 		auto container = new Widget(bar);
310 
311 		// and these just add color to the containers we made above for the screenshot.
312 		// in a real application, you can just add your actual controls instead of these.
313 		auto headerColorBox = new ColorWidget(Color.teal, header);
314 		auto leftColorBox = new ColorWidget(Color.green, left);
315 		auto rightColorBox = new ColorWidget(Color.purple, container);
316 
317 		window.loop();
318 	}
319 
320 	main(); // exclude from docs
321 }
322 
323 
324 import arsd.core;
325 alias Timer = arsd.simpledisplay.Timer;
326 public import arsd.simpledisplay;
327 /++
328 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
329 
330 	History:
331 		Was private until May 15, 2021.
332 +/
333 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
334 
335 version(Windows) {
336 	import core.sys.windows.winnls;
337 	import core.sys.windows.windef;
338 	import core.sys.windows.basetyps;
339 	import core.sys.windows.winbase;
340 	import core.sys.windows.winuser;
341 	import core.sys.windows.wingdi;
342 	static import gdi = core.sys.windows.wingdi;
343 }
344 
345 version(Windows) {
346 	version(minigui_manifest) {} else version=minigui_no_manifest;
347 
348 	version(minigui_no_manifest) {} else
349 	static if(__VERSION__ >= 2_083)
350 	version(CRuntime_Microsoft) { // FIXME: mingw?
351 		// assume we want commctrl6 whenever possible since there's really no reason not to
352 		// and this avoids some of the manifest hassle
353 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
354 	}
355 }
356 
357 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
358 private bool lastDefaultPrevented;
359 
360 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
361 alias scriptable = arsd_jsvar_compatible;
362 
363 version(Windows) {
364 	// use native widgets when available unless specifically asked otherwise
365 	version(custom_widgets) {
366 		enum bool UsingCustomWidgets = true;
367 		enum bool UsingWin32Widgets = false;
368 	} else {
369 		version = win32_widgets;
370 		enum bool UsingCustomWidgets = false;
371 		enum bool UsingWin32Widgets = true;
372 
373 		// give access to my text system for the rich text cross platform stuff
374 		version = use_new_text_system;
375 		import arsd.textlayouter;
376 	}
377 	// and native theming when needed
378 	//version = win32_theming;
379 } else {
380 	enum bool UsingCustomWidgets = true;
381 	enum bool UsingWin32Widgets = false;
382 	version=custom_widgets;
383 }
384 
385 
386 
387 /*
388 
389 	The main goals of minigui.d are to:
390 		1) Provide basic widgets that just work in a lightweight lib.
391 		   I basically want things comparable to a plain HTML form,
392 		   plus the easy and obvious things you expect from Windows
393 		   apps like a menu.
394 		2) Use native things when possible for best functionality with
395 		   least library weight.
396 		3) Give building blocks to provide easy extension for your
397 		   custom widgets, or hooking into additional native widgets
398 		   I didn't wrap.
399 		4) Provide interfaces for easy interaction between third
400 		   party minigui extensions. (event model, perhaps
401 		   signals/slots, drop-in ease of use bits.)
402 		5) Zero non-system dependencies, including Phobos as much as
403 		   I reasonably can. It must only import arsd.color and
404 		   my simpledisplay.d. If you need more, it will have to be
405 		   an extension module.
406 		6) An easy layout system that generally works.
407 
408 	A stretch goal is to make it easy to make gui forms with code,
409 	some kind of resource file (xml?) and even a wysiwyg designer.
410 
411 	Another stretch goal is to make it easy to hook data into the gui,
412 	including from reflection. So like auto-generate a form from a
413 	function signature or struct definition, or show a list from an
414 	array that automatically updates as the array is changed. Then,
415 	your program focuses on the data more than the gui interaction.
416 
417 
418 
419 	STILL NEEDED:
420 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
421 		* slider
422 		* listbox
423 		* spinner
424 		* label?
425 		* rich text
426 */
427 
428 
429 /+
430 	enum LayoutMethods {
431 		 verticalFlex,
432 		 horizontalFlex,
433 		 inlineBlock, // left to right, no stretch, goes to next line as needed
434 		 static, // just set to x, y
435 		 verticalNoStretch, // browser style default
436 
437 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
438 
439 		 grid, // magic
440 	}
441 +/
442 
443 /++
444 	The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own.
445 
446 
447 	To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional.
448 
449 	---
450 	class MinimalWidget : Widget {
451 		this(Widget parent) {
452 			super(parent);
453 		}
454 	}
455 	---
456 
457 	$(SIDEBAR
458 		I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway.
459 	)
460 
461 	Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal.
462 
463 	Among the things you'll most likely want to change in your custom widget:
464 
465 	$(LIST
466 		* In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.)
467 
468 		You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now.
469 
470 		Do this $(I after) calling the `super` constructor.
471 
472 		* Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget.
473 
474 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
475 
476 		* Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model.
477 
478 		* You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want.
479 	)
480 
481 	On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s.
482 
483 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
484 
485 	Your own custom-drawn and native system controls can exist side-by-side.
486 
487 	Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas.
488 +/
489 class Widget : ReflectableProperties {
490 
491 	private bool willDraw() {
492 		return true;
493 	}
494 
495 	/+
496 	/++
497 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
498 
499 		History:
500 			Added September 15, 2021
501 			implemented.... ???
502 	+/
503 	void prepareReflection(this This)() {
504 
505 	}
506 	+/
507 
508 	private bool _enabled = true;
509 
510 	/++
511 		Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled.
512 
513 		I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it.
514 
515 		History:
516 			Added November 23, 2021 (dub v10.4)
517 
518 			Warning: the specific behavior of disabling with parents may change in the future.
519 		Bugs:
520 			Currently only implemented for widgets backed by native Windows controls.
521 
522 		See_Also: [disabledReason], [disabledBy]
523 	+/
524 	@property bool enabled() {
525 		return disabledBy() is null;
526 	}
527 
528 	/// ditto
529 	@property void enabled(bool yes) {
530 		_enabled = yes;
531 		version(win32_widgets) {
532 			if(hwnd)
533 				EnableWindow(hwnd, yes);
534 		}
535 		setDynamicState(DynamicState.disabled, yes);
536 	}
537 
538 	private string disabledReason_;
539 
540 	/++
541 		If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control.
542 
543 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
544 
545 		History:
546 			Added November 23, 2021 (dub v10.4)
547 		See_Also: [enabled], [disabledBy]
548 	+/
549 	@property string disabledReason() {
550 		auto w = disabledBy();
551 		return (w is null) ? null : w.disabledReason_;
552 	}
553 
554 	/// ditto
555 	@property void disabledReason(string reason) {
556 		disabledReason_ = reason;
557 	}
558 
559 	/++
560 		Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user.
561 
562 		History:
563 			Added November 25, 2021 (dub v10.4)
564 		See_Also: [enabled], [disabledReason]
565 	+/
566 	Widget disabledBy() {
567 		Widget p = this;
568 		while(p) {
569 			if(!p._enabled)
570 				return p;
571 			p = p.parent;
572 		}
573 		return null;
574 	}
575 
576 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
577 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
578 		if(valueIsJson)
579 			return SetPropertyResult.wrongFormat;
580 		switch(name) {
581 			case "name":
582 				this.name = value.idup;
583 				return SetPropertyResult.success;
584 			case "statusTip":
585 				this.statusTip = value.idup;
586 				return SetPropertyResult.success;
587 			default:
588 				return SetPropertyResult.noSuchProperty;
589 		}
590 	}
591 	/// ditto
592 	void getPropertiesList(scope void delegate(string name) sink) const {
593 		sink("name");
594 		sink("statusTip");
595 	}
596 	/// ditto
597 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
598 		switch(name) {
599 			case "name":
600 				sink(name, this.name, false);
601 				return;
602 			case "statusTip":
603 				sink(name, this.statusTip, false);
604 				return;
605 			default:
606 				sink(name, null, true);
607 		}
608 	}
609 
610 	/++
611 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
612 
613 		History:
614 			Added November 25, 2021 (dub v10.5)
615 			`Point` overload added January 12, 2022 (dub v10.6)
616 	+/
617 	int scaleWithDpi(int value, int assumedDpi = 96) {
618 		// avoid potential overflow with common special values
619 		if(value == int.max)
620 			return int.max;
621 		if(value == int.min)
622 			return int.min;
623 		if(value == 0)
624 			return 0;
625 		return value * currentDpi(assumedDpi) / assumedDpi;
626 	}
627 
628 	/// ditto
629 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
630 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
631 	}
632 
633 	/++
634 		Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor.
635 
636 		Not entirely stable.
637 
638 		History:
639 			Added August 25, 2023 (dub v11.1)
640 	+/
641 	final int currentDpi(int assumedDpi = 96) {
642 		// assert(parentWindow !is null);
643 		// assert(parentWindow.win !is null);
644 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
645 		//divide = 138; // to test 1.5x
646 		// for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down.
647 		// this also covers the case when actualDpi returns 0.
648 		if(divide < 96)
649 			divide = 96;
650 		return divide;
651 	}
652 
653 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
654 	// I'll think up something better eventually
655 
656 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
657 	protected final int defaultLineHeight() {
658 		auto cs = getComputedStyle();
659 		if(cs.font && !cs.font.isNull)
660 			return cs.font.height() * 5 / 4;
661 		else
662 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
663 	}
664 
665 	/++
666 
667 		History:
668 			Added August 25, 2023 (dub v11.1)
669 	+/
670 	protected final int defaultTextHeight(int numberOfLines = 1) {
671 		auto cs = getComputedStyle();
672 		if(cs.font && !cs.font.isNull)
673 			return cs.font.height() * numberOfLines;
674 		else
675 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
676 	}
677 
678 	protected final int defaultTextWidth(const(char)[] text) {
679 		auto cs = getComputedStyle();
680 		if(cs.font && !cs.font.isNull)
681 			return cs.font.stringWidth(text);
682 		else
683 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
684 	}
685 
686 	/++
687 		If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget.
688 
689 		The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you.
690 
691 		History:
692 			Added May 22, 2021
693 	+/
694 	protected bool encapsulatedChildren() {
695 		return false;
696 	}
697 
698 	private void privateDpiChanged() {
699 		dpiChanged();
700 		foreach(child; children)
701 			child.privateDpiChanged();
702 	}
703 
704 	/++
705 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
706 
707 		History:
708 			Added January 12, 2022 (dub v10.6)
709 	+/
710 	protected void dpiChanged() {
711 
712 	}
713 
714 	// Default layout properties {
715 
716 		int minWidth() { return 0; }
717 		int minHeight() {
718 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
719 			int sum = this.paddingTop + this.paddingBottom;
720 			foreach(child; children) {
721 				if(child.hidden)
722 					continue;
723 				sum += child.minHeight();
724 				sum += child.marginTop();
725 				sum += child.marginBottom();
726 			}
727 
728 			return sum;
729 		}
730 		int maxWidth() { return int.max; }
731 		int maxHeight() { return int.max; }
732 		int widthStretchiness() { return 4; }
733 		int heightStretchiness() { return 4; }
734 
735 		/++
736 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
737 
738 			History:
739 				Added June 15, 2021 (dub v10.1)
740 		+/
741 		int widthShrinkiness() { return 0; }
742 		/// ditto
743 		int heightShrinkiness() { return 0; }
744 
745 		/++
746 			The initial size of the widget for layout calculations. Default is 0.
747 
748 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
749 
750 			History:
751 				Added June 15, 2021 (dub v10.1)
752 		+/
753 		int flexBasisWidth() { return 0; }
754 		/// ditto
755 		int flexBasisHeight() { return 0; }
756 
757 		/++
758 			Not stable.
759 
760 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
761 
762 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
763 
764 			History:
765 				Added January 5, 2023
766 		+/
767 		Rectangle defaultMargin;
768 		/// ditto
769 		Rectangle defaultPadding;
770 
771 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
772 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
773 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
774 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
775 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
776 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
777 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
778 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
779 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
780 
781 		private bool recomputeChildLayoutRequired = true;
782 		private static class RecomputeEvent {}
783 		private __gshared rce = new RecomputeEvent();
784 		protected final void queueRecomputeChildLayout() {
785 			recomputeChildLayoutRequired = true;
786 
787 			if(this.parentWindow) {
788 				auto sw = this.parentWindow.win;
789 				assert(sw !is null);
790 				if(!sw.eventQueued!RecomputeEvent) {
791 					sw.postEvent(rce);
792 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
793 				}
794 			}
795 
796 		}
797 
798 		protected final void recomputeChildLayoutEntry() {
799 			if(recomputeChildLayoutRequired) {
800 				recomputeChildLayout();
801 				recomputeChildLayoutRequired = false;
802 				redraw();
803 			} else {
804 				// I still need to check the tree just in case one of them was queued up
805 				// and the event came up here instead of there.
806 				foreach(child; children)
807 					child.recomputeChildLayoutEntry();
808 			}
809 		}
810 
811 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
812 		void recomputeChildLayout() {
813 			.recomputeChildLayout!"height"(this);
814 		}
815 
816 	// }
817 
818 
819 	/++
820 		Returns the style's tag name string this object uses.
821 
822 		The default is to use the typeid() name trimmed down to whatever is after the last dot which is typically the identifier of the class.
823 
824 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
825 
826 		History:
827 			Added May 10, 2021
828 	+/
829 	string styleTagName() const {
830 		string n = typeid(this).name;
831 		foreach_reverse(idx, ch; n)
832 			if(ch == '.') {
833 				n = n[idx + 1 .. $];
834 				break;
835 			}
836 		return n;
837 	}
838 
839 	/// API for the [styleClassList]
840 	static struct ClassList {
841 		private Widget widget;
842 
843 		///
844 		void add(string s) {
845 			widget.styleClassList_ ~= s;
846 		}
847 
848 		///
849 		void remove(string s) {
850 			foreach(idx, s1; widget.styleClassList_)
851 				if(s1 == s) {
852 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
853 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
854 					widget.styleClassList_.assumeSafeAppend();
855 					return;
856 				}
857 		}
858 
859 		/// Returns true if it was added, false if it was removed.
860 		bool toggle(string s) {
861 			if(contains(s)) {
862 				remove(s);
863 				return false;
864 			} else {
865 				add(s);
866 				return true;
867 			}
868 		}
869 
870 		///
871 		bool contains(string s) const {
872 			foreach(s1; widget.styleClassList_)
873 				if(s1 == s)
874 					return true;
875 			return false;
876 
877 		}
878 	}
879 
880 	private string[] styleClassList_;
881 
882 	/++
883 		Returns a "class list" that can be used by the visual theme's style engine via [VisualTheme.getPropertyString] if it chooses to do something like CSS.
884 
885 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
886 
887 		History:
888 			Added May 10, 2021
889 	+/
890 	inout(ClassList) styleClassList() inout {
891 		return cast(inout(ClassList)) ClassList(cast() this);
892 	}
893 
894 	/++
895 		List of dynamic states made available to the style engine, for cases like CSS pseudo-classes and also used by default paint methods. It is stored in a 64 bit variable attached to the widget that you can update. The style cache is aware of the fact that these can frequently change.
896 
897 		The lower 32 bits are defined here or reserved for future use by the library. You should keep these updated if you reasonably can on custom widgets if they apply to you, but don't use them for a purpose they aren't defined for.
898 
899 		The upper 32 bits are available for your own extensions.
900 
901 		History:
902 			Added May 10, 2021
903 	+/
904 	enum DynamicState : ulong {
905 		focus = (1 << 0), /// the widget currently has the keyboard focus
906 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
907 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!)
908 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!)
909 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
910 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
911 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
912 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
913 		depressed = (1 << 8), /// the widget is being actively pressed or clicked (compare to css `:active`). Can be combined with hover to visually indicate if a mouse up would result in a click event.
914 
915 		USER_BEGIN = (1UL << 32),
916 	}
917 
918 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
919 
920 	/// ditto
921 	@property ulong dynamicState() { return dynamicState_; }
922 	/// ditto
923 	@property ulong dynamicState(ulong newValue) {
924 		if(dynamicState != newValue) {
925 			auto old = dynamicState_;
926 			dynamicState_ = newValue;
927 
928 			useStyleProperties((scope Widget.Style s) {
929 				if(s.variesWithState(old ^ newValue))
930 					redraw();
931 			});
932 		}
933 		return dynamicState_;
934 	}
935 
936 	/// ditto
937 	void setDynamicState(ulong flags, bool state) {
938 		auto ds = dynamicState_;
939 		if(state)
940 			ds |= flags;
941 		else
942 			ds &= ~flags;
943 
944 		dynamicState = ds;
945 	}
946 
947 	private ulong dynamicState_;
948 
949 	deprecated("Use dynamic styles instead now") {
950 		Color backgroundColor() { return backgroundColor_; }
951 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
952 
953 		MouseCursor cursor() { return GenericCursor.Default; }
954 	} private Color backgroundColor_ = Color.transparent;
955 
956 
957 	/++
958 		Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such).
959 
960 		It is here so there can be a specificity switch.
961 
962 		See [OverrideStyle] for a helper function to use your own.
963 
964 		History:
965 			Added May 11, 2021
966 	+/
967 	static class Style/* : StyleProperties*/ {
968 		public Widget widget; // public because the mixin template needs access to it
969 
970 		/++
971 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
972 
973 			History:
974 				Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect.
975 		+/
976 		bool variesWithState(ulong dynamicStateFlags) {
977 			version(win32_widgets) {
978 				if(widget.hwnd)
979 					return false;
980 			}
981 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
982 		}
983 
984 		///
985 		Color foregroundColor() {
986 			return WidgetPainter.visualTheme.foregroundColor;
987 		}
988 
989 		///
990 		WidgetBackground background() {
991 			// the default is a "transparent" background, which means
992 			// it goes as far up as it can to get the color
993 			if (widget.backgroundColor_ != Color.transparent)
994 				return WidgetBackground(widget.backgroundColor_);
995 			if (widget.parent)
996 				return widget.parent.getComputedStyle.background;
997 			return WidgetBackground(widget.backgroundColor_);
998 		}
999 
1000 		private static OperatingSystemFont fontCached_;
1001 		private OperatingSystemFont fontCached() {
1002 			if(fontCached_ is null)
1003 				fontCached_ = font();
1004 			return fontCached_;
1005 		}
1006 
1007 		/++
1008 			Returns the default font to be used with this widget. The return value will be cached by the library, so you can not expect live updates.
1009 		+/
1010 		OperatingSystemFont font() {
1011 			return null;
1012 		}
1013 
1014 		/++
1015 			Returns the cursor that should be used over this widget. You may change this and updates will be reflected next time the mouse enters the widget.
1016 
1017 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1018 
1019 			History:
1020 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1021 		+/
1022 		MouseCursor cursor() {
1023 			return GenericCursor.Default;
1024 		}
1025 
1026 		FrameStyle borderStyle() {
1027 			return FrameStyle.none;
1028 		}
1029 
1030 		/++
1031 		+/
1032 		Color borderColor() {
1033 			return Color.transparent;
1034 		}
1035 
1036 		FrameStyle outlineStyle() {
1037 			if(widget.dynamicState & DynamicState.focus)
1038 				return FrameStyle.dotted;
1039 			else
1040 				return FrameStyle.none;
1041 		}
1042 
1043 		Color outlineColor() {
1044 			return foregroundColor;
1045 		}
1046 	}
1047 
1048 	/++
1049 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1050 		The basic usage is simple:
1051 
1052 		---
1053 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1054 			// override style hints as-needed here
1055 		}
1056 		OverrideStyle!Style; // add the method
1057 		---
1058 
1059 		$(TIP
1060 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1061 			can not be inherited by other objects whereas the static one can. A property on the base class,
1062 			called [Widget.Style.widget|widget], is available for you to access its properties.
1063 		)
1064 
1065 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1066 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1067 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1068 
1069 
1070 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1071 		You may also just override `variesWithState` when you use this flag.
1072 
1073 		---
1074 		mixin OverrideStyle!(
1075 			DynamicState.focus, YourFocusedStyle,
1076 			DynamicState.hover, YourHoverStyle,
1077 			YourDefaultStyle
1078 		)
1079 		---
1080 
1081 		It checks if `dynamicState` matches the state and if so, returns the object given.
1082 
1083 		If there is no state mask given, the next one matches everything. The first match given is used.
1084 
1085 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1086 		find much use for this whole-class swap out.
1087 
1088 		History:
1089 			Added May 16, 2021
1090 	+/
1091 	static protected mixin template OverrideStyle(S...) {
1092 		static import amg = arsd.minigui;
1093 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1094 			ulong mask = 0;
1095 			foreach(idx, thing; S) {
1096 				static if(is(typeof(thing) : ulong)) {
1097 					mask = thing;
1098 				} else {
1099 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1100 						//static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class.");
1101 						scope amg.Widget.Style s = new thing();
1102 						s.widget = this;
1103 						dg(s);
1104 						return;
1105 					}
1106 				}
1107 			}
1108 		}
1109 	}
1110 	/++
1111 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1112 	+/
1113 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1114 		scope Style s = new Style();
1115 		s.widget = this;
1116 		dg(s);
1117 	}
1118 
1119 
1120 	protected void sendResizeEvent() {
1121 		this.emit!ResizeEvent();
1122 	}
1123 
1124 	Menu contextMenu(int x, int y) { return null; }
1125 
1126 	final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) {
1127 		if(parentWindow is null || parentWindow.win is null) return false;
1128 
1129 		auto menu = this.contextMenu(x, y);
1130 		if(menu is null)
1131 			return false;
1132 
1133 		version(win32_widgets) {
1134 			// FIXME: if it is -1, -1, do it at the current selection location instead
1135 			// tho the corner of the window, whcih it does now, isn't the literal worst.
1136 
1137 			if(screenX < 0 && screenY < 0) {
1138 				auto p = this.globalCoordinates();
1139 				if(screenX == -2)
1140 					p.x += x;
1141 				if(screenY == -2)
1142 					p.y += y;
1143 
1144 				screenX = p.x;
1145 				screenY = p.y;
1146 			}
1147 
1148 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1149 				throw new Exception("TrackContextMenuEx");
1150 		} else version(custom_widgets) {
1151 			menu.popup(this, x, y);
1152 		}
1153 
1154 		return true;
1155 	}
1156 
1157 	/++
1158 		Removes this widget from its parent.
1159 
1160 		History:
1161 			`removeWidget` was made `final` on May 11, 2021.
1162 	+/
1163 	@scriptable
1164 	final void removeWidget() {
1165 		auto p = this.parent;
1166 		if(p) {
1167 			int item;
1168 			for(item = 0; item < p._children.length; item++)
1169 				if(p._children[item] is this)
1170 					break;
1171 			auto idx = item;
1172 			for(; item < p._children.length - 1; item++)
1173 				p._children[item] = p._children[item + 1];
1174 			p._children = p._children[0 .. $-1];
1175 
1176 			this.parent.widgetRemoved(idx, this);
1177 			//this.parent = null;
1178 
1179 			p.queueRecomputeChildLayout();
1180 		}
1181 		version(win32_widgets) {
1182 			removeAllChildren();
1183 			if(hwnd) {
1184 				DestroyWindow(hwnd);
1185 				hwnd = null;
1186 			}
1187 		}
1188 	}
1189 
1190 	/++
1191 		Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync.
1192 
1193 		History:
1194 			Added September 19, 2021
1195 	+/
1196 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1197 
1198 	/++
1199 		Removes all child widgets from `this`. You should not use the removed widgets again.
1200 
1201 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1202 
1203 		History:
1204 			Added July 1, 2021 (dub v10.2)
1205 	+/
1206 	void removeAllChildren() {
1207 		version(win32_widgets)
1208 		foreach(child; _children) {
1209 			child.removeAllChildren();
1210 			if(child.hwnd) {
1211 				DestroyWindow(child.hwnd);
1212 				child.hwnd = null;
1213 			}
1214 		}
1215 		auto orig = this._children;
1216 		this._children = null;
1217 		foreach(idx, w; orig)
1218 			this.widgetRemoved(idx, w);
1219 
1220 		queueRecomputeChildLayout();
1221 	}
1222 
1223 	/++
1224 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1225 	+/
1226 	@scriptable
1227 	Widget getChildByName(string name) {
1228 		return getByName(name);
1229 	}
1230 	/++
1231 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1232 	+/
1233 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1234 		if(this.name == name)
1235 			if(auto c = cast(WidgetClass) this)
1236 				return c;
1237 		foreach(child; children) {
1238 			auto w = child.getByName(name);
1239 			if(auto c = cast(WidgetClass) w)
1240 				return c;
1241 		}
1242 		return null;
1243 	}
1244 
1245 	/++
1246 		The name is a string tag that is used to reference the widget from scripts, gui loaders, declarative ui templates, etc. Similar to a HTML id attribute.
1247 		Names should be unique in a window.
1248 
1249 		See_Also: [getByName], [getChildByName]
1250 	+/
1251 	@scriptable string name;
1252 
1253 	private EventHandler[][string] bubblingEventHandlers;
1254 	private EventHandler[][string] capturingEventHandlers;
1255 
1256 	/++
1257 		Default event handlers. These are called on the appropriate
1258 		event unless [Event.preventDefault] is called on the event at
1259 		some point through the bubbling process.
1260 
1261 
1262 		If you are implementing your own widget and want to add custom
1263 		events, you should follow the same pattern here: create a virtual
1264 		function named `defaultEventHandler_eventname` with the implementation,
1265 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1266 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1267 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1268 		This ensures virtual dispatch based on the correct subclass.
1269 
1270 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1271 		overridden version.
1272 
1273 		You only need to do that on parent classes adding NEW event types. If you
1274 		just want to change the default behavior of an existing event type in a subclass,
1275 		you override the function (and optionally call `super.method_name`) like normal.
1276 
1277 	+/
1278 	protected EventHandler[string] defaultEventHandlers;
1279 
1280 	/// ditto
1281 	void setupDefaultEventHandlers() {
1282 		defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); };
1283 		defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); };
1284 		defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); };
1285 		defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); };
1286 		defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); };
1287 		defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); };
1288 		defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); };
1289 		defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); };
1290 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); };
1291 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); };
1292 		defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); };
1293 		defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); };
1294 		defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); };
1295 		defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); };
1296 		defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); };
1297 		defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); };
1298 		defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); };
1299 		defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); };
1300 	}
1301 
1302 	/// ditto
1303 	void defaultEventHandler_click(ClickEvent event) {}
1304 	/// ditto
1305 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1306 	/// ditto
1307 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1308 	/// ditto
1309 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1310 	/// ditto
1311 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1312 		if(event.button == MouseButton.left) {
1313 			if(this.tabStop) {
1314 				this.focus();
1315 			}
1316 		}
1317 	}
1318 	/// ditto
1319 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1320 	/// ditto
1321 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1322 	/// ditto
1323 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1324 	/// ditto
1325 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1326 	/// ditto
1327 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1328 	/// ditto
1329 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1330 	/// ditto
1331 	void defaultEventHandler_char(CharEvent event) {}
1332 	/// ditto
1333 	void defaultEventHandler_triggered(Event event) {}
1334 	/// ditto
1335 	void defaultEventHandler_change(Event event) {}
1336 	/// ditto
1337 	void defaultEventHandler_focus(Event event) {}
1338 	/// ditto
1339 	void defaultEventHandler_blur(Event event) {}
1340 	/// ditto
1341 	void defaultEventHandler_focusin(Event event) {}
1342 	/// ditto
1343 	void defaultEventHandler_focusout(Event event) {}
1344 
1345 	/++
1346 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1347 
1348 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1349 
1350 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1351 		of participating in handler delegation.
1352 
1353 		$(TIP
1354 			Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need.
1355 		)
1356 	+/
1357 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1358 		return addEventListener(event, (Widget, scope Event e) {
1359 			if(e.srcElement is this)
1360 				handler();
1361 		}, useCapture);
1362 	}
1363 
1364 	/// ditto
1365 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1366 		return addEventListener(event, (Widget, Event e) {
1367 			if(e.srcElement is this)
1368 				handler(e);
1369 		}, useCapture);
1370 	}
1371 
1372 	/// ditto
1373 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1374 		static if(is(Handler Fn == delegate)) {
1375 		static if(is(Fn Params == __parameters)) {
1376 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1377 				if(e.srcElement !is this)
1378 					return;
1379 				auto ty = cast(Params[0]) e;
1380 				if(ty !is null)
1381 					handler(ty);
1382 			}, useCapture);
1383 		} else static assert(0);
1384 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1385 	}
1386 
1387 	/// ditto
1388 	@scriptable
1389 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1390 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1391 	}
1392 
1393 	/// ditto
1394 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1395 		static if(is(Handler Fn == delegate)) {
1396 		static if(is(Fn Params == __parameters)) {
1397 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1398 				auto ty = cast(Params[0]) e;
1399 				if(ty !is null)
1400 					handler(ty);
1401 			}, useCapture);
1402 		} else static assert(0);
1403 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1404 	}
1405 
1406 	/// ditto
1407 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1408 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1409 	}
1410 
1411 	/// ditto
1412 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1413 		if(event.length > 2 && event[0..2] == "on")
1414 			event = event[2 .. $];
1415 
1416 		if(useCapture)
1417 			capturingEventHandlers[event] ~= handler;
1418 		else
1419 			bubblingEventHandlers[event] ~= handler;
1420 
1421 		return EventListener(this, event, handler, useCapture);
1422 	}
1423 
1424 	/// ditto
1425 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1426 		if(event.length > 2 && event[0..2] == "on")
1427 			event = event[2 .. $];
1428 
1429 		if(useCapture) {
1430 			if(event in capturingEventHandlers)
1431 			foreach(ref evt; capturingEventHandlers[event])
1432 				if(evt is handler) evt = null;
1433 		} else {
1434 			if(event in bubblingEventHandlers)
1435 			foreach(ref evt; bubblingEventHandlers[event])
1436 				if(evt is handler) evt = null;
1437 		}
1438 	}
1439 
1440 	/// ditto
1441 	void removeEventListener(EventListener listener) {
1442 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1443 	}
1444 
1445 	static if(UsingSimpledisplayX11) {
1446 		void discardXConnectionState() {
1447 			foreach(child; children)
1448 				child.discardXConnectionState();
1449 		}
1450 
1451 		void recreateXConnectionState() {
1452 			foreach(child; children)
1453 				child.recreateXConnectionState();
1454 			redraw();
1455 		}
1456 	}
1457 
1458 	/++
1459 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1460 
1461 		History:
1462 			`globalCoordinates` was made `final` on May 11, 2021.
1463 	+/
1464 	Point globalCoordinates() {
1465 		int x = this.x;
1466 		int y = this.y;
1467 		auto p = this.parent;
1468 		while(p) {
1469 			x += p.x;
1470 			y += p.y;
1471 			p = p.parent;
1472 		}
1473 
1474 		static if(UsingSimpledisplayX11) {
1475 			auto dpy = XDisplayConnection.get;
1476 			arsd.simpledisplay.Window dummyw;
1477 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1478 		} else version(Windows) {
1479 			POINT pt;
1480 			pt.x = x;
1481 			pt.y = y;
1482 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1483 			x = pt.x;
1484 			y = pt.y;
1485 		} else {
1486 			featureNotImplemented();
1487 		}
1488 
1489 		return Point(x, y);
1490 	}
1491 
1492 	version(win32_widgets)
1493 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1494 
1495 	version(win32_widgets)
1496 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1497 	void handleWmCommand(ushort cmd, ushort id) {}
1498 
1499 	version(win32_widgets)
1500 	/++
1501 		Called when a WM_NOTIFY is sent to the associated hwnd.
1502 
1503 		History:
1504 	+/
1505 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1506 
1507 	version(win32_widgets)
1508 	deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); }
1509 
1510 	/++
1511 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1512 
1513 		Updates to this variable will only be made visible on the next mouse enter event.
1514 	+/
1515 	@scriptable string statusTip;
1516 	// string toolTip;
1517 	// string helpText;
1518 
1519 	/++
1520 		If true, this widget can be focused via keyboard control with the tab key.
1521 
1522 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1523 	+/
1524 	bool tabStop = true;
1525 	/++
1526 		The tab key cycles through widgets by the order of a.tabOrder < b.tabOrder. If they are equal, it does them in child order (which is typically the order they were added to the widget.)
1527 	+/
1528 	int tabOrder;
1529 
1530 	version(win32_widgets) {
1531 		static Widget[HWND] nativeMapping;
1532 		/// The native handle, if there is one.
1533 		HWND hwnd;
1534 		WNDPROC originalWindowProcedure;
1535 
1536 		SimpleWindow simpleWindowWrappingHwnd;
1537 
1538 		// please note it IGNORES your return value and does NOT forward it to Windows!
1539 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1540 			return 0;
1541 		}
1542 	}
1543 	private bool implicitlyCreated;
1544 
1545 	/// Child's position relative to the parent's origin. only the layout manager should be modifying this and even reading it is of limited utility. It may be made `private` at some point in the future without advance notice. Do NOT depend on it being available unless you are writing a layout manager.
1546 	int x;
1547 	/// ditto
1548 	int y;
1549 	private int _width;
1550 	private int _height;
1551 	private Widget[] _children;
1552 	private Widget _parent;
1553 	private Window _parentWindow;
1554 
1555 	/++
1556 		Returns the window to which this widget is attached.
1557 
1558 		History:
1559 			Prior to May 11, 2021, the `Window parentWindow` variable was directly available. Now, only this property getter is available and the actual store is private.
1560 	+/
1561 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1562 	private @property void parentWindow(Window parent) {
1563 		auto old = _parentWindow;
1564 		_parentWindow = parent;
1565 		newParentWindow(old, _parentWindow);
1566 		foreach(child; children)
1567 			child.parentWindow = parent; // please note that this is recursive
1568 	}
1569 
1570 	/++
1571 		Called when the widget has been added to or remove from a parent window.
1572 
1573 		Note that either oldParent and/or newParent may be null any time this is called.
1574 
1575 		History:
1576 			Added September 13, 2024
1577 	+/
1578 	protected void newParentWindow(Window oldParent, Window newParent) {}
1579 
1580 	/++
1581 		Returns the list of the widget's children.
1582 
1583 		History:
1584 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1585 
1586 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1587 	+/
1588 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1589 
1590 	/++
1591 		Returns the widget's parent.
1592 
1593 		History:
1594 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1595 
1596 			The parent should only be managed by the [addChild] and [removeWidget] method.
1597 	+/
1598 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1599 
1600 	/// The widget's current size.
1601 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1602 	/// ditto
1603 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1604 
1605 	/// Only the layout manager should be calling these.
1606 	final protected @property int width(int a) @safe { return _width = a; }
1607 	/// ditto
1608 	final protected @property int height(int a) @safe { return _height = a; }
1609 
1610 	/++
1611 		This function is called by the layout engine after it has updated the position (in variables `x` and `y`) and the size (in properties `width` and `height`) to give you a chance to update the actual position of the native child window (if there is one) or whatever.
1612 
1613 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
1614 	+/
1615 	protected void registerMovement() {
1616 		version(win32_widgets) {
1617 			if(hwnd) {
1618 				auto pos = getChildPositionRelativeToParentHwnd(this);
1619 				MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn
1620 				this.redraw();
1621 			}
1622 		}
1623 		sendResizeEvent();
1624 	}
1625 
1626 	/// Creates the widget and adds it to the parent.
1627 	this(Widget parent) {
1628 		if(parent !is null)
1629 			parent.addChild(this);
1630 		setupDefaultEventHandlers();
1631 	}
1632 
1633 	/// Returns true if this is the current focused widget inside the parent window. Please note it may return `true` when the window itself is unfocused. In that case, it indicates this widget will receive focuse again when the window does.
1634 	@scriptable
1635 	bool isFocused() {
1636 		return parentWindow && parentWindow.focusedWidget is this;
1637 	}
1638 
1639 	private bool showing_ = true;
1640 	///
1641 	bool showing() { return showing_; }
1642 	///
1643 	bool hidden() { return !showing_; }
1644 	/++
1645 		Shows or hides the window. Meant to be assigned as a property. If `recalculate` is true (the default), it recalculates the layout of the parent widget to use the space this widget being hidden frees up or make space for this widget to appear again.
1646 	+/
1647 	void showing(bool s, bool recalculate = true) {
1648 		auto so = showing_;
1649 		showing_ = s;
1650 		if(s != so) {
1651 			version(win32_widgets)
1652 			if(hwnd)
1653 				ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE);
1654 
1655 			if(parent && recalculate) {
1656 				parent.queueRecomputeChildLayout();
1657 				parent.redraw();
1658 			}
1659 
1660 			foreach(child; children)
1661 				child.showing(s, false);
1662 
1663 		}
1664 		queueRecomputeChildLayout();
1665 		redraw();
1666 	}
1667 	/// Convenience method for `showing = true`
1668 	@scriptable
1669 	void show() {
1670 		showing = true;
1671 	}
1672 	/// Convenience method for `showing = false`
1673 	@scriptable
1674 	void hide() {
1675 		showing = false;
1676 	}
1677 
1678 	///
1679 	@scriptable
1680 	void focus() {
1681 		assert(parentWindow !is null);
1682 		if(isFocused())
1683 			return;
1684 
1685 		if(parentWindow.focusedWidget) {
1686 			// FIXME: more details here? like from and to
1687 			auto from = parentWindow.focusedWidget;
1688 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
1689 			parentWindow.focusedWidget = null;
1690 			from.emit!BlurEvent();
1691 			this.emit!FocusOutEvent();
1692 		}
1693 
1694 
1695 		version(win32_widgets) {
1696 			if(this.hwnd !is null)
1697 				SetFocus(this.hwnd);
1698 		}
1699 		//else static if(UsingSimpledisplayX11)
1700 			//this.parentWindow.win.focus();
1701 
1702 		parentWindow.focusedWidget = this;
1703 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
1704 		this.emit!FocusEvent();
1705 		this.emit!FocusInEvent();
1706 	}
1707 
1708 	/+
1709 	/++
1710 		Unfocuses the widget. This may reset
1711 	+/
1712 	@scriptable
1713 	void blur() {
1714 
1715 	}
1716 	+/
1717 
1718 
1719 	/++
1720 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
1721 
1722 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
1723 	+/
1724 	void attachedToWindow(Window w) {}
1725 	/++
1726 		Callback when the widget is added to another widget.
1727 
1728 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
1729 	+/
1730 	void addedTo(Widget w) {}
1731 
1732 	/++
1733 		Adds a child to the given position. This is `protected` because you generally shouldn't be calling this directly. Instead, construct widgets with the parent directly.
1734 
1735 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
1736 	+/
1737 	protected void addChild(Widget w, int position = int.max) {
1738 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
1739 		assert(w !is this, "Child cannot be its own parent!");
1740 		w._parent = this;
1741 		if(position == int.max || position == children.length) {
1742 			_children ~= w;
1743 		} else {
1744 			assert(position < _children.length);
1745 			_children.length = _children.length + 1;
1746 			for(int i = cast(int) _children.length - 1; i > position; i--)
1747 				_children[i] = _children[i - 1];
1748 			_children[position] = w;
1749 		}
1750 
1751 		this.parentWindow = this._parentWindow;
1752 
1753 		w.addedTo(this);
1754 
1755 		if(this.hidden)
1756 			w.showing = false;
1757 
1758 		if(parentWindow !is null) {
1759 			w.attachedToWindow(parentWindow);
1760 			parentWindow.queueRecomputeChildLayout();
1761 			parentWindow.redraw();
1762 		}
1763 	}
1764 
1765 	/++
1766 		Finds the child at the top of the z-order at the given coordinates (relative to the `this` widget's origin), or null if none are found.
1767 	+/
1768 	Widget getChildAtPosition(int x, int y) {
1769 		// it goes backward so the last one to show gets picked first
1770 		// might use z-index later
1771 		foreach_reverse(child; children) {
1772 			if(child.hidden)
1773 				continue;
1774 			if(child.x <= x && child.y <= y
1775 				&& ((x - child.x) < child.width)
1776 				&& ((y - child.y) < child.height))
1777 			{
1778 				return child;
1779 			}
1780 		}
1781 
1782 		return null;
1783 	}
1784 
1785 	/++
1786 		If the widget is a scrollable container, this should add the current scroll position to the given coordinates so the mouse events can be dispatched correctly.
1787 
1788 		History:
1789 			Added July 2, 2021 (v10.2)
1790 	+/
1791 	protected void addScrollPosition(ref int x, ref int y) {};
1792 
1793 	/++
1794 		Responsible for actually painting the widget to the screen. The clip rectangle and coordinate translation in the [WidgetPainter] are pre-configured so you can draw independently.
1795 
1796 		This function paints the entire widget, including styled borders, backgrounds, etc. You are also responsible for displaying any important active state to the user, including if you hold the active keyboard focus. If you only want to be responsible for the content while letting the style engine draw the rest, override [paintContent] instead.
1797 
1798 		[paint] is not called for system widgets as the OS library draws them instead.
1799 
1800 
1801 		The default implementation forwards to [WidgetPainter.drawThemed], passing [paintContent] as the delegate. If you override this, you might use those same functions or you can do your own thing.
1802 
1803 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
1804 
1805 		History:
1806 			Prior to May 15, 2021, the default implementation was empty. Now, it is `painter.drawThemed(&paintContent);`. You may wish to override [paintContent] instead of [paint] to take advantage of the new styling engine.
1807 	+/
1808 	void paint(WidgetPainter painter) {
1809 		version(win32_widgets)
1810 			if(hwnd) {
1811 				return;
1812 			}
1813 		painter.drawThemed(&paintContent); // note this refers to the following overload
1814 	}
1815 
1816 	/++
1817 		Responsible for drawing the content as the theme engine is responsible for other elements.
1818 
1819 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
1820 
1821 		Params:
1822 			painter = your painter (forwarded from [paint]) for drawing on the widget. The clip rectangle and coordinate translation are prepared for you ahead of time so you can use widget coordinates. It also has the theme foreground preloaded into the painter outline color, the theme font preloaded as the painter's active font, and the theme background preloaded as the painter's fill color.
1823 
1824 			bounds = the bounds, inside the widget, where your content should be drawn. This is the rectangle inside the border and padding (if any). The stuff outside is not clipped - it is still part of your widget - but you should respect these bounds for visual consistency and respecting the theme's area.
1825 
1826 			If you do want to clip it, you can of course call `auto oldClip = painter.setClipRectangle(bounds); scope(exit) painter.setClipRectangle(oldClip);` to modify it and return to the previous setting when you return.
1827 
1828 		Returns:
1829 			The rectangle representing your actual content. Typically, this is simply `return bounds;`. The theme engine uses this return value to determine where the outline and overlay should be.
1830 
1831 		History:
1832 			Added May 15, 2021
1833 	+/
1834 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
1835 		return bounds;
1836 	}
1837 
1838 	deprecated("Change ScreenPainter to WidgetPainter")
1839 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
1840 
1841 	/// I don't actually like the name of this
1842 	/// this draws a background on it
1843 	void erase(WidgetPainter painter) {
1844 		version(win32_widgets)
1845 			if(hwnd) return; // Windows will do it. I think.
1846 
1847 		auto c = getComputedStyle().background.color;
1848 		painter.fillColor = c;
1849 		painter.outlineColor = c;
1850 
1851 		version(win32_widgets) {
1852 			HANDLE b, p;
1853 			if(c.a == 0 && parent is parentWindow) {
1854 				// I don't remember why I had this really...
1855 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
1856 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
1857 			}
1858 		}
1859 		painter.drawRectangle(Point(0, 0), width, height);
1860 		version(win32_widgets) {
1861 			if(c.a == 0 && parent is parentWindow) {
1862 				SelectObject(painter.impl.hdc, p);
1863 				SelectObject(painter.impl.hdc, b);
1864 			}
1865 		}
1866 	}
1867 
1868 	///
1869 	WidgetPainter draw() {
1870 		int x = this.x, y = this.y;
1871 		auto parent = this.parent;
1872 		while(parent) {
1873 			x += parent.x;
1874 			y += parent.y;
1875 			parent = parent.parent;
1876 		}
1877 
1878 		auto painter = parentWindow.win.draw(true);
1879 		painter.originX = x;
1880 		painter.originY = y;
1881 		painter.setClipRectangle(Point(0, 0), width, height);
1882 		return WidgetPainter(painter, this);
1883 	}
1884 
1885 	/// This can be overridden by scroll things. It is responsible for actually calling [paint]. Do not override unless you've studied minigui.d's source code. There are no stability guarantees if you do override this; it can (and likely will) break without notice.
1886 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
1887 		if(hidden)
1888 			return;
1889 
1890 		int paintX = x;
1891 		int paintY = y;
1892 		if(this.useNativeDrawing()) {
1893 			paintX = 0;
1894 			paintY = 0;
1895 			lox = 0;
1896 			loy = 0;
1897 			containment = Rectangle(0, 0, int.max, int.max);
1898 		}
1899 
1900 		painter.originX = lox + paintX;
1901 		painter.originY = loy + paintY;
1902 
1903 		bool actuallyPainted = false;
1904 
1905 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
1906 		if(clip == Rectangle.init) {
1907 			// writeln(this, " clipped out");
1908 			return;
1909 		}
1910 
1911 		bool invalidateChildren = invalidate;
1912 
1913 		if(redrawRequested || force) {
1914 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
1915 
1916 			painter.drawingUpon = this;
1917 
1918 			erase(painter);
1919 			if(painter.visualTheme)
1920 				painter.visualTheme.doPaint(this, painter);
1921 			else
1922 				paint(painter);
1923 
1924 			if(invalidate) {
1925 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
1926 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
1927 				painter.invalidateRect(region);
1928 				// children are contained inside this, so no need to do extra work
1929 				invalidateChildren = false;
1930 			}
1931 
1932 			redrawRequested = false;
1933 			actuallyPainted = true;
1934 		}
1935 
1936 		foreach(child; children) {
1937 			version(win32_widgets)
1938 				if(child.useNativeDrawing()) continue;
1939 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
1940 		}
1941 
1942 		version(win32_widgets)
1943 		foreach(child; children) {
1944 			if(child.useNativeDrawing) {
1945 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
1946 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, true); // have to reset the invalidate flag since these are not necessarily affected the same way, being native children with a clip
1947 			}
1948 		}
1949 	}
1950 
1951 	protected bool useNativeDrawing() nothrow {
1952 		version(win32_widgets)
1953 			return hwnd !is null;
1954 		else
1955 			return false;
1956 	}
1957 
1958 	private static class RedrawEvent {}
1959 	private __gshared re = new RedrawEvent();
1960 
1961 	private bool redrawRequested;
1962 	///
1963 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
1964 		redrawRequested = true;
1965 
1966 		if(this.parentWindow) {
1967 			auto sw = this.parentWindow.win;
1968 			assert(sw !is null);
1969 			if(!sw.eventQueued!RedrawEvent) {
1970 				sw.postEvent(re);
1971 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1972 			}
1973 		}
1974 	}
1975 
1976 	private SimpleWindow drawableWindow;
1977 
1978 	/++
1979 		Allows a class to easily dispatch its own statically-declared event (see [Emits]). The main benefit of using this over constructing an event yourself is simply that you ensure you haven't sent something you haven't documented you can send.
1980 
1981 		Returns:
1982 			`true` if you should do your default behavior.
1983 
1984 		History:
1985 			Added May 5, 2021
1986 
1987 		Bugs:
1988 			It does not do the static checks on gdc right now.
1989 	+/
1990 	final protected bool emit(EventType, this This, Args...)(Args args) {
1991 		version(GNU) {} else
1992 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
1993 		auto e = new EventType(this, args);
1994 		e.dispatch();
1995 		return !e.defaultPrevented;
1996 	}
1997 	/// ditto
1998 	final protected bool emit(string eventString, this This)() {
1999 		auto e = new Event(eventString, this);
2000 		e.dispatch();
2001 		return !e.defaultPrevented;
2002 	}
2003 
2004 	/++
2005 		Does the same as [addEventListener]'s delegate overload, but adds an additional check to ensure the event you are subscribing to is actually emitted by the static type you are using. Since it works on static types, if you have a generic [Widget], this can only subscribe to events declared as [Emits] inside [Widget] itself, not any child classes nor any child elements. If this is too restrictive, simply use [addEventListener] instead.
2006 
2007 		History:
2008 			Added May 5, 2021
2009 	+/
2010 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2011 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2012 		return addEventListener(handler);
2013 	}
2014 
2015 	/++
2016 		Gets the computed style properties from the visual theme.
2017 
2018 		You should use this in your paint and layout functions instead of the direct properties on the widget if you want to be style aware. (But when setting defaults in your classes, overriding is the right thing to do. Override to set defaults, but then read out of [getComputedStyle].)
2019 
2020 		History:
2021 			Added May 8, 2021
2022 	+/
2023 	final StyleInformation getComputedStyle() {
2024 		return StyleInformation(this);
2025 	}
2026 
2027 	int focusableWidgets(scope int delegate(Widget) dg) {
2028 		foreach(widget; WidgetStream(this)) {
2029 			if(widget.tabStop && !widget.hidden) {
2030 				int result = dg(widget);
2031 				if (result)
2032 					return result;
2033 			}
2034 		}
2035 		return 0;
2036 	}
2037 
2038 	/++
2039 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2040 		for the given content box (the area between the padding)
2041 
2042 		History:
2043 			Added January 4, 2023 (dub v11.0)
2044 	+/
2045 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2046 		auto cs = getComputedStyle();
2047 
2048 		auto borderWidth = getBorderWidth(cs.borderStyle);
2049 
2050 		auto rect = contentBox;
2051 
2052 		rect.left -= borderWidth;
2053 		rect.right += borderWidth;
2054 		rect.top -= borderWidth;
2055 		rect.bottom += borderWidth;
2056 
2057 		auto insideBorderRect = rect;
2058 
2059 		rect.left -= cs.paddingLeft;
2060 		rect.right += cs.paddingRight;
2061 		rect.top -= cs.paddingTop;
2062 		rect.bottom += cs.paddingBottom;
2063 
2064 		return rect;
2065 	}
2066 
2067 
2068 	// FIXME: I kinda want to hide events from implementation widgets
2069 	// so it just catches them all and stops propagation...
2070 	// i guess i can do it with a event listener on star.
2071 
2072 	mixin Emits!KeyDownEvent; ///
2073 	mixin Emits!KeyUpEvent; ///
2074 	mixin Emits!CharEvent; ///
2075 
2076 	mixin Emits!MouseDownEvent; ///
2077 	mixin Emits!MouseUpEvent; ///
2078 	mixin Emits!ClickEvent; ///
2079 	mixin Emits!DoubleClickEvent; ///
2080 	mixin Emits!MouseMoveEvent; ///
2081 	mixin Emits!MouseOverEvent; ///
2082 	mixin Emits!MouseOutEvent; ///
2083 	mixin Emits!MouseEnterEvent; ///
2084 	mixin Emits!MouseLeaveEvent; ///
2085 
2086 	mixin Emits!ResizeEvent; ///
2087 
2088 	mixin Emits!BlurEvent; ///
2089 	mixin Emits!FocusEvent; ///
2090 
2091 	mixin Emits!FocusInEvent; ///
2092 	mixin Emits!FocusOutEvent; ///
2093 }
2094 
2095 /+
2096 /++
2097 	Interface to indicate that the widget has a simple value property.
2098 
2099 	History:
2100 		Added August 26, 2021
2101 +/
2102 interface HasValue!T {
2103 	/// Getter
2104 	@property T value();
2105 	/// Setter
2106 	@property void value(T);
2107 }
2108 
2109 /++
2110 	Interface to indicate that the widget has a range of possible values for its simple value property.
2111 	This would be present on something like a slider or possibly a number picker.
2112 
2113 	History:
2114 		Added September 11, 2021
2115 +/
2116 interface HasRangeOfValues!T : HasValue!T {
2117 	/// The minimum and maximum values in the range, inclusive.
2118 	@property T minValue();
2119 	@property void minValue(T); /// ditto
2120 	@property T maxValue(); /// ditto
2121 	@property void maxValue(T); /// ditto
2122 
2123 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2124 	@property void step(T);
2125 	@property T step(); /// ditto
2126 }
2127 
2128 /++
2129 	Interface to indicate that the widget has a list of possible values the user can choose from.
2130 	This would be present on something like a drop-down selector.
2131 
2132 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2133 	combobox.
2134 
2135 	History:
2136 		Added September 11, 2021
2137 +/
2138 interface HasListOfValues!T : HasValue!T {
2139 	@property T[] values;
2140 	@property void values(T[]);
2141 
2142 	@property int selectedIndex(); // note it may return -1!
2143 	@property void selectedIndex(int);
2144 }
2145 +/
2146 
2147 /++
2148 	History:
2149 		Added September 2021 (dub v10.4)
2150 +/
2151 class GridLayout : Layout {
2152 
2153 	// FIXME: grid padding around edges and also cell spacing between units. even though you could do that by just specifying some gutter yourself in the layout.
2154 
2155 	/++
2156 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2157 	+/
2158 	enum Gravity {
2159 		Center    = 0,
2160 		NorthWest = North | West,
2161 		North     = 0b10_00,
2162 		NorthEast = North | East,
2163 		West      = 0b00_10,
2164 		East      = 0b00_01,
2165 		SouthWest = South | West,
2166 		South     = 0b01_00,
2167 		SouthEast = South | East,
2168 	}
2169 
2170 	/++
2171 		The width and height are in some proportional units and can often just be 12.
2172 	+/
2173 	this(int width, int height, Widget parent) {
2174 		this.gridWidth = width;
2175 		this.gridHeight = height;
2176 		super(parent);
2177 	}
2178 
2179 	/++
2180 		Sets the position of the given child.
2181 
2182 		The units of these arguments are in the proportional grid units you set in the constructor.
2183 	+/
2184 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2185 		// ensure it is in bounds
2186 		// then ensure no overlaps
2187 
2188 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2189 
2190 		foreach(ref position; positions) {
2191 			if(position.widget is child) {
2192 				position = p;
2193 				goto set;
2194 			}
2195 		}
2196 
2197 		positions ~= p;
2198 
2199 		set:
2200 
2201 		// FIXME: should this batch?
2202 		queueRecomputeChildLayout();
2203 
2204 		return child;
2205 	}
2206 
2207 	override void addChild(Widget w, int position = int.max) {
2208 		super.addChild(w, position);
2209 		//positions ~= ChildPosition(w);
2210 		if(position != int.max) {
2211 			// FIXME: align it so they actually match.
2212 		}
2213 	}
2214 
2215 	override void widgetRemoved(size_t idx, Widget w) {
2216 		// FIXME: keep the positions array aligned
2217 		// positions[idx].widget = null;
2218 	}
2219 
2220 	override void recomputeChildLayout() {
2221 		registerMovement();
2222 		int onGrid = cast(int) positions.length;
2223 		c: foreach(child; children) {
2224 			// just snap it to the grid
2225 			if(onGrid)
2226 			foreach(position; positions)
2227 				if(position.widget is child) {
2228 					child.x = this.width * position.x / this.gridWidth;
2229 					child.y = this.height * position.y / this.gridHeight;
2230 					child.width = this.width * position.width / this.gridWidth;
2231 					child.height = this.height * position.height / this.gridHeight;
2232 
2233 					auto diff = child.width - child.maxWidth();
2234 					// FIXME: gravity?
2235 					if(diff > 0) {
2236 						child.width = child.width - diff;
2237 
2238 						if(position.gravity & Gravity.West) {
2239 							// nothing needed, already aligned
2240 						} else if(position.gravity & Gravity.East) {
2241 							child.x += diff;
2242 						} else {
2243 							child.x += diff / 2;
2244 						}
2245 					}
2246 
2247 					diff = child.height - child.maxHeight();
2248 					// FIXME: gravity?
2249 					if(diff > 0) {
2250 						child.height = child.height - diff;
2251 
2252 						if(position.gravity & Gravity.North) {
2253 							// nothing needed, already aligned
2254 						} else if(position.gravity & Gravity.South) {
2255 							child.y += diff;
2256 						} else {
2257 							child.y += diff / 2;
2258 						}
2259 					}
2260 
2261 
2262 					child.recomputeChildLayout();
2263 					onGrid--;
2264 					continue c;
2265 				}
2266 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2267 		}
2268 	}
2269 
2270 	private struct ChildPosition {
2271 		Widget widget;
2272 		int x;
2273 		int y;
2274 		int width;
2275 		int height;
2276 		Gravity gravity;
2277 	}
2278 	private ChildPosition[] positions;
2279 
2280 	int gridWidth = 12;
2281 	int gridHeight = 12;
2282 }
2283 
2284 ///
2285 abstract class ComboboxBase : Widget {
2286 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2287 	// or to always show the list, we want CBS_SIMPLE == 1
2288 	version(win32_widgets)
2289 		this(uint style, Widget parent) {
2290 			super(parent);
2291 			createWin32Window(this, "ComboBox"w, null, style);
2292 		}
2293 	else version(custom_widgets)
2294 		this(Widget parent) {
2295 			super(parent);
2296 
2297 			addEventListener((KeyDownEvent event) {
2298 				if(event.key == Key.Up) {
2299 					if(selection_ > -1) { // -1 means select blank
2300 						selection_--;
2301 						fireChangeEvent();
2302 					}
2303 					event.preventDefault();
2304 				}
2305 				if(event.key == Key.Down) {
2306 					if(selection_ + 1 < options.length) {
2307 						selection_++;
2308 						fireChangeEvent();
2309 					}
2310 					event.preventDefault();
2311 				}
2312 
2313 			});
2314 
2315 		}
2316 	else static assert(false);
2317 
2318 	/++
2319 		Returns the current list of options in the selection.
2320 
2321 		History:
2322 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2323 	+/
2324 	final @property string[] options() const {
2325 		return cast(string[]) options_;
2326 	}
2327 
2328 	private string[] options_;
2329 	private int selection_ = -1;
2330 
2331 	/++
2332 		Adds an option to the end of options array.
2333 	+/
2334 	void addOption(string s) {
2335 		options_ ~= s;
2336 		version(win32_widgets)
2337 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2338 	}
2339 
2340 	/++
2341 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2342 	+/
2343 	int getSelection() {
2344 		return selection_;
2345 	}
2346 
2347 	/++
2348 		Returns the current selection as a string.
2349 
2350 		History:
2351 			Added November 17, 2021
2352 	+/
2353 	string getSelectionString() {
2354 		return selection_ == -1 ? null : options[selection_];
2355 	}
2356 
2357 	/++
2358 		Sets the current selection to an index in the options array, or to the given option if present.
2359 		Please note that the string version may do a linear lookup.
2360 
2361 		Returns:
2362 			the index you passed in
2363 
2364 		History:
2365 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2366 
2367 			The return value was `void` prior to March 1, 2022.
2368 	+/
2369 	int setSelection(int idx) {
2370 		selection_ = idx;
2371 		version(win32_widgets)
2372 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2373 
2374 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2375 		t.dispatch();
2376 
2377 		return idx;
2378 	}
2379 
2380 	/// ditto
2381 	int setSelection(string s) {
2382 		if(s !is null)
2383 		foreach(idx, item; options)
2384 			if(item == s) {
2385 				return setSelection(cast(int) idx);
2386 			}
2387 		return setSelection(-1);
2388 	}
2389 
2390 	/++
2391 		This event is fired when the selection changes. Note it inherits
2392 		from ChangeEvent!string, meaning you can use that as well, and it also
2393 		fills in [Event.intValue].
2394 	+/
2395 	static class SelectionChangedEvent : ChangeEvent!string {
2396 		this(Widget target, int iv, string sv) {
2397 			super(target, &stringValue);
2398 			this.iv = iv;
2399 			this.sv = sv;
2400 		}
2401 		immutable int iv;
2402 		immutable string sv;
2403 
2404 		override @property string stringValue() { return sv; }
2405 		override @property int intValue() { return iv; }
2406 	}
2407 
2408 	version(win32_widgets)
2409 	override void handleWmCommand(ushort cmd, ushort id) {
2410 		if(cmd == CBN_SELCHANGE) {
2411 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2412 			fireChangeEvent();
2413 		}
2414 	}
2415 
2416 	private void fireChangeEvent() {
2417 		if(selection_ >= options.length)
2418 			selection_ = -1;
2419 
2420 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2421 		t.dispatch();
2422 	}
2423 
2424 	version(win32_widgets) {
2425 		override int minHeight() { return defaultLineHeight + 6; }
2426 		override int maxHeight() { return defaultLineHeight + 6; }
2427 	} else {
2428 		override int minHeight() { return defaultLineHeight + 4; }
2429 		override int maxHeight() { return defaultLineHeight + 4; }
2430 	}
2431 
2432 	version(custom_widgets) {
2433 
2434 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2435 
2436 		SimpleWindow dropDown;
2437 		void popup() {
2438 			auto w = width;
2439 			// FIXME: suggestedDropdownHeight see below
2440 			auto h = cast(int) this.options.length * defaultLineHeight + 8;
2441 
2442 			auto coord = this.globalCoordinates();
2443 			auto dropDown = new SimpleWindow(
2444 				w, h,
2445 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null);
2446 
2447 			dropDown.move(coord.x, coord.y + this.height);
2448 
2449 			{
2450 				auto cs = getComputedStyle();
2451 				auto painter = dropDown.draw();
2452 				draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2453 				auto p = Point(4, 4);
2454 				painter.outlineColor = cs.foregroundColor;
2455 				foreach(option; options) {
2456 					painter.drawText(p, option);
2457 					p.y += defaultLineHeight;
2458 				}
2459 			}
2460 
2461 			dropDown.setEventHandlers(
2462 				(MouseEvent event) {
2463 					if(event.type == MouseEventType.buttonReleased) {
2464 						dropDown.close();
2465 						auto element = (event.y - 4) / defaultLineHeight;
2466 						if(element >= 0 && element <= options.length) {
2467 							selection_ = element;
2468 
2469 							fireChangeEvent();
2470 						}
2471 					}
2472 				}
2473 			);
2474 
2475 			dropDown.visibilityChanged = (bool visible) {
2476 				if(visible) {
2477 					this.redraw();
2478 					dropDown.grabInput();
2479 				} else {
2480 					dropDown.releaseInputGrab();
2481 				}
2482 			};
2483 
2484 			dropDown.show();
2485 		}
2486 
2487 	}
2488 }
2489 
2490 /++
2491 	A drop-down list where the user must select one of the
2492 	given options. Like `<select>` in HTML.
2493 +/
2494 class DropDownSelection : ComboboxBase {
2495 	this(Widget parent) {
2496 		version(win32_widgets)
2497 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
2498 		else version(custom_widgets) {
2499 			super(parent);
2500 
2501 			addEventListener("focus", () { this.redraw; });
2502 			addEventListener("blur", () { this.redraw; });
2503 			addEventListener(EventType.change, () { this.redraw; });
2504 			addEventListener("mousedown", () { this.focus(); this.popup(); });
2505 			addEventListener((KeyDownEvent event) {
2506 				if(event.key == Key.Space)
2507 					popup();
2508 			});
2509 		} else static assert(false);
2510 	}
2511 
2512 	mixin Padding!q{2};
2513 	static class Style : Widget.Style {
2514 		override FrameStyle borderStyle() { return FrameStyle.risen; }
2515 	}
2516 	mixin OverrideStyle!Style;
2517 
2518 	version(custom_widgets)
2519 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2520 		auto cs = getComputedStyle();
2521 
2522 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
2523 
2524 		painter.outlineColor = cs.foregroundColor;
2525 		painter.fillColor = cs.foregroundColor;
2526 
2527 		/+
2528 		Point[4] triangle;
2529 		enum padding = 6;
2530 		enum paddingV = 7;
2531 		enum triangleWidth = 10;
2532 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
2533 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
2534 		triangle[2] = Point(width - padding - 0, paddingV);
2535 		triangle[3] = triangle[0];
2536 		painter.drawPolygon(triangle[]);
2537 		+/
2538 
2539 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
2540 
2541 		painter.drawPolygon(
2542 			scaleWithDpi(Point(2, 6) + offset),
2543 			scaleWithDpi(Point(7, 11) + offset),
2544 			scaleWithDpi(Point(12, 6) + offset),
2545 			scaleWithDpi(Point(2, 6) + offset)
2546 		);
2547 
2548 
2549 		return bounds;
2550 	}
2551 
2552 	version(win32_widgets)
2553 	override void registerMovement() {
2554 		version(win32_widgets) {
2555 			if(hwnd) {
2556 				auto pos = getChildPositionRelativeToParentHwnd(this);
2557 				// the height given to this from Windows' perspective is supposed
2558 				// to include the drop down's height. so I add to it to give some
2559 				// room for that.
2560 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
2561 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
2562 			}
2563 		}
2564 		sendResizeEvent();
2565 	}
2566 }
2567 
2568 /++
2569 	A text box with a drop down arrow listing selections.
2570 	The user can choose from the list, or type their own.
2571 +/
2572 class FreeEntrySelection : ComboboxBase {
2573 	this(Widget parent) {
2574 		version(win32_widgets)
2575 			super(2 /* CBS_DROPDOWN */, parent);
2576 		else version(custom_widgets) {
2577 			super(parent);
2578 			auto hl = new HorizontalLayout(this);
2579 			lineEdit = new LineEdit(hl);
2580 
2581 			tabStop = false;
2582 
2583 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2584 
2585 			auto btn = new class ArrowButton {
2586 				this() {
2587 					super(ArrowDirection.down, hl);
2588 				}
2589 				override int maxHeight() {
2590 					return lineEdit.maxHeight;
2591 				}
2592 			};
2593 			//btn.addDirectEventListener("focus", &lineEdit.focus);
2594 			btn.addEventListener("triggered", &this.popup);
2595 			addEventListener(EventType.change, (Event event) {
2596 				lineEdit.content = event.stringValue;
2597 				lineEdit.focus();
2598 				redraw();
2599 			});
2600 		}
2601 		else static assert(false);
2602 	}
2603 
2604 	version(custom_widgets) {
2605 		LineEdit lineEdit;
2606 	}
2607 }
2608 
2609 /++
2610 	A combination of free entry with a list below it.
2611 +/
2612 class ComboBox : ComboboxBase {
2613 	this(Widget parent) {
2614 		version(win32_widgets)
2615 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
2616 		else version(custom_widgets) {
2617 			super(parent);
2618 			lineEdit = new LineEdit(this);
2619 			listWidget = new ListWidget(this);
2620 			listWidget.multiSelect = false;
2621 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
2622 				string c = null;
2623 				foreach(option; listWidget.options)
2624 					if(option.selected) {
2625 						c = option.label;
2626 						break;
2627 					}
2628 				lineEdit.content = c;
2629 			});
2630 
2631 			listWidget.tabStop = false;
2632 			this.tabStop = false;
2633 			listWidget.addEventListener("focus", &lineEdit.focus);
2634 			this.addEventListener("focus", &lineEdit.focus);
2635 
2636 			addDirectEventListener(EventType.change, {
2637 				listWidget.setSelection(selection_);
2638 				if(selection_ != -1)
2639 					lineEdit.content = options[selection_];
2640 				lineEdit.focus();
2641 				redraw();
2642 			});
2643 
2644 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2645 
2646 			listWidget.addDirectEventListener(EventType.change, {
2647 				int set = -1;
2648 				foreach(idx, opt; listWidget.options)
2649 					if(opt.selected) {
2650 						set = cast(int) idx;
2651 						break;
2652 					}
2653 				if(set != selection_)
2654 					this.setSelection(set);
2655 			});
2656 		} else static assert(false);
2657 	}
2658 
2659 	override int minHeight() { return defaultLineHeight * 3; }
2660 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
2661 	override int heightStretchiness() { return 5; }
2662 
2663 	version(custom_widgets) {
2664 		LineEdit lineEdit;
2665 		ListWidget listWidget;
2666 
2667 		override void addOption(string s) {
2668 			listWidget.options ~= ListWidget.Option(s);
2669 			ComboboxBase.addOption(s);
2670 		}
2671 	}
2672 }
2673 
2674 /+
2675 class Spinner : Widget {
2676 	version(win32_widgets)
2677 	this(Widget parent) {
2678 		super(parent);
2679 		parentWindow = parent.parentWindow;
2680 		auto hlayout = new HorizontalLayout(this);
2681 		lineEdit = new LineEdit(hlayout);
2682 		upDownControl = new UpDownControl(hlayout);
2683 	}
2684 
2685 	LineEdit lineEdit;
2686 	UpDownControl upDownControl;
2687 }
2688 
2689 class UpDownControl : Widget {
2690 	version(win32_widgets)
2691 	this(Widget parent) {
2692 		super(parent);
2693 		parentWindow = parent.parentWindow;
2694 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
2695 	}
2696 
2697 	override int minHeight() { return defaultLineHeight; }
2698 	override int maxHeight() { return defaultLineHeight * 3/2; }
2699 
2700 	override int minWidth() { return defaultLineHeight * 3/2; }
2701 	override int maxWidth() { return defaultLineHeight * 3/2; }
2702 }
2703 +/
2704 
2705 /+
2706 class DataView : Widget {
2707 	// this is the omnibus data viewer
2708 	// the internal data layout is something like:
2709 	// string[string][] but also each node can have parents
2710 }
2711 +/
2712 
2713 
2714 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
2715 
2716 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
2717 
2718 // FIXME: menus should prolly capture the mouse. ugh i kno.
2719 /*
2720 	TextEdit needs:
2721 
2722 	* caret manipulation
2723 	* selection control
2724 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
2725 
2726 	For example:
2727 
2728 	connect(paste, &textEdit.insertTextAtCaret);
2729 
2730 	would be nice.
2731 
2732 
2733 
2734 	I kinda want an omnibus dataview that combines list, tree,
2735 	and table - it can be switched dynamically between them.
2736 
2737 	Flattening policy: only show top level, show recursive, show grouped
2738 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
2739 
2740 	Single select, multi select, organization, drag+drop
2741 */
2742 
2743 //static if(UsingSimpledisplayX11)
2744 version(win32_widgets) {}
2745 else version(custom_widgets) {
2746 	enum scrollClickRepeatInterval = 50;
2747 
2748 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
2749 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
2750 	enum activeTabColor = lightAccentColor;
2751 	enum hoveringColor = Color(228, 228, 228);
2752 	enum buttonColor = windowBackgroundColor;
2753 	enum depressedButtonColor = darkAccentColor;
2754 	enum activeListXorColor = Color(255, 255, 127);
2755 	enum progressBarColor = Color(0, 0, 128);
2756 	enum activeMenuItemColor = Color(0, 0, 128);
2757 
2758 }}
2759 else static assert(false);
2760 deprecated("Get these properties off the `visualTheme` instead.") {
2761 	// these are used by horizontal rule so not just custom_widgets. for now at least.
2762 	enum darkAccentColor = Color(172, 172, 172);
2763 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
2764 }
2765 
2766 private const(wchar)* toWstringzInternal(in char[] s) {
2767 	wchar[] str;
2768 	str.reserve(s.length + 1);
2769 	foreach(dchar ch; s)
2770 		str ~= ch;
2771 	str ~= '\0';
2772 	return str.ptr;
2773 }
2774 
2775 static if(SimpledisplayTimerAvailable)
2776 void setClickRepeat(Widget w, int interval, int delay = 250) {
2777 	Timer timer;
2778 	int delayRemaining = delay / interval;
2779 	if(delayRemaining <= 1)
2780 		delayRemaining = 2;
2781 
2782 	immutable originalDelayRemaining = delayRemaining;
2783 
2784 	w.addDirectEventListener((scope MouseDownEvent ev) {
2785 		if(ev.srcElement !is w)
2786 			return;
2787 		if(timer !is null) {
2788 			timer.destroy();
2789 			timer = null;
2790 		}
2791 		delayRemaining = originalDelayRemaining;
2792 		timer = new Timer(interval, () {
2793 			if(delayRemaining > 0)
2794 				delayRemaining--;
2795 			else {
2796 				auto ev = new Event("triggered", w);
2797 				ev.sendDirectly();
2798 			}
2799 		});
2800 	});
2801 
2802 	w.addDirectEventListener((scope MouseUpEvent ev) {
2803 		if(ev.srcElement !is w)
2804 			return;
2805 		if(timer !is null) {
2806 			timer.destroy();
2807 			timer = null;
2808 		}
2809 	});
2810 
2811 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
2812 		if(ev.srcElement !is w)
2813 			return;
2814 		if(timer !is null) {
2815 			timer.destroy();
2816 			timer = null;
2817 		}
2818 	});
2819 
2820 }
2821 else
2822 void setClickRepeat(Widget w, int interval, int delay = 250) {}
2823 
2824 enum FrameStyle {
2825 	none, ///
2826 	risen, /// a 3d pop-out effect (think Windows 95 button)
2827 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
2828 	solid, ///
2829 	dotted, ///
2830 	fantasy, /// a style based on a popular fantasy video game
2831 	rounded, /// a rounded rectangle
2832 }
2833 
2834 version(custom_widgets)
2835 deprecated
2836 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
2837 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2838 }
2839 
2840 version(custom_widgets)
2841 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
2842 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
2843 }
2844 
2845 version(custom_widgets)
2846 deprecated
2847 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
2848 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2849 }
2850 
2851 int getBorderWidth(FrameStyle style) {
2852 	final switch(style) {
2853 		case FrameStyle.sunk, FrameStyle.risen:
2854 			return 2;
2855 		case FrameStyle.none:
2856 			return 0;
2857 		case FrameStyle.solid:
2858 			return 1;
2859 		case FrameStyle.dotted:
2860 			return 1;
2861 		case FrameStyle.fantasy:
2862 			return 3;
2863 		case FrameStyle.rounded:
2864 			return 2;
2865 	}
2866 }
2867 
2868 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
2869 	int borderWidth = getBorderWidth(style);
2870 	final switch(style) {
2871 		case FrameStyle.sunk, FrameStyle.risen:
2872 			// outer layer
2873 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
2874 		break;
2875 		case FrameStyle.none:
2876 			painter.outlineColor = background;
2877 		break;
2878 		case FrameStyle.solid:
2879 		case FrameStyle.rounded:
2880 			painter.pen = Pen(border, 1);
2881 		break;
2882 		case FrameStyle.dotted:
2883 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
2884 		break;
2885 		case FrameStyle.fantasy:
2886 			painter.pen = Pen(border, 3);
2887 		break;
2888 	}
2889 
2890 	painter.fillColor = background;
2891 
2892 	if(style == FrameStyle.rounded) {
2893 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
2894 	} else {
2895 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
2896 
2897 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
2898 			// 3d effect
2899 			auto vt = WidgetPainter.visualTheme;
2900 
2901 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
2902 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
2903 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
2904 
2905 			// inner layer
2906 			//right, bottom
2907 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
2908 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
2909 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
2910 			// left, top
2911 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
2912 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
2913 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
2914 		} else if(style == FrameStyle.fantasy) {
2915 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
2916 			painter.fillColor = Color.transparent;
2917 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
2918 		}
2919 	}
2920 
2921 	return borderWidth;
2922 }
2923 
2924 /++
2925 	An `Action` represents some kind of user action they can trigger through menu options, toolbars, hotkeys, and similar mechanisms. The text label, icon, and handlers are centrally held here instead of repeated in each UI element.
2926 
2927 	See_Also:
2928 		[MenuItem]
2929 		[ToolButton]
2930 		[Menu.addItem]
2931 +/
2932 class Action {
2933 	version(win32_widgets) {
2934 		private int id;
2935 		private static int lastId = 9000;
2936 		private static Action[int] mapping;
2937 	}
2938 
2939 	KeyEvent accelerator;
2940 
2941 	// FIXME: disable message
2942 	// and toggle thing?
2943 	// ??? and trigger arguments too ???
2944 
2945 	/++
2946 		Params:
2947 			label = the textual label
2948 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
2949 			triggered = initial handler, more can be added via the [triggered] member.
2950 	+/
2951 	this(string label, ushort icon = 0, void delegate() triggered = null) {
2952 		this.label = label;
2953 		this.iconId = icon;
2954 		if(triggered !is null)
2955 			this.triggered ~= triggered;
2956 		version(win32_widgets) {
2957 			id = ++lastId;
2958 			mapping[id] = this;
2959 		}
2960 	}
2961 
2962 	private string label;
2963 	private ushort iconId;
2964 	// icon
2965 
2966 	// when it is triggered, the triggered event is fired on the window
2967 	/// The list of handlers when it is triggered.
2968 	void delegate()[] triggered;
2969 }
2970 
2971 /*
2972 	plan:
2973 		keyboard accelerators
2974 
2975 		* menus (and popups and tooltips)
2976 		* status bar
2977 		* toolbars and buttons
2978 
2979 		sortable table view
2980 
2981 		maybe notification area icons
2982 		basic clipboard
2983 
2984 		* radio box
2985 		splitter
2986 		toggle buttons (optionally mutually exclusive, like in Paint)
2987 		label, rich text display, multi line plain text (selectable)
2988 		* fieldset
2989 		* nestable grid layout
2990 		single line text input
2991 		* multi line text input
2992 		slider
2993 		spinner
2994 		list box
2995 		drop down
2996 		combo box
2997 		auto complete box
2998 		* progress bar
2999 
3000 		terminal window/widget (on unix it might even be a pty but really idk)
3001 
3002 		ok button
3003 		cancel button
3004 
3005 		keyboard hotkeys
3006 
3007 		scroll widget
3008 
3009 		event redirections and network transparency
3010 		script integration
3011 */
3012 
3013 
3014 /*
3015 	MENUS
3016 
3017 	auto bar = new MenuBar(window);
3018 	window.menuBar = bar;
3019 
3020 	auto fileMenu = bar.addItem(new Menu("&File"));
3021 	fileMenu.addItem(new MenuItem("&Exit"));
3022 
3023 
3024 	EVENTS
3025 
3026 	For controls, you should usually use "triggered" rather than "click", etc., because
3027 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3028 	This is the case on menus and pushbuttons.
3029 
3030 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3031 */
3032 
3033 
3034 /*
3035 enum LinePreference {
3036 	AlwaysOnOwnLine, // always on its own line
3037 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3038 	PreferToShareLine, // does not force new line, and if the next child likes to share too, they will div it up evenly. otherwise, it will expand as much as it can
3039 }
3040 */
3041 
3042 /++
3043 	Convenience mixin for overriding all four sides of margin or padding in a [Widget] with the same code. It mixes in the given string as the return value of the four overridden methods.
3044 
3045 	---
3046 	class MyWidget : Widget {
3047 		this(Widget parent) { super(parent); }
3048 
3049 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3050 		mixin Padding!q{4};
3051 
3052 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3053 		mixin Margin!q{8};
3054 
3055 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3056 		// while Top/Bottom/Right remain 8 from the mixin above.
3057 		override int marginLeft() { return 2; }
3058 	}
3059 	---
3060 
3061 
3062 	The minigui layout model is based on the web's CSS box model. The layout engine* arranges widgets based on their margin for separation and assigns them a size based on thier preferences (e.g. [Widget.minHeight]) and the available space. Widgets are assigned a size by the layout engine. Inside this size, they have a border (see [Widget.Style.borderWidth]), then padding space, and then their content. Their content box may also have an outline drawn on top of it (see [Widget.Style.outlineStyle]).
3063 
3064 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3065 
3066 	Margin is the area between widgets. The algorithm is the spacing between any two widgets is the max of their adjacent margins (not the sum!).
3067 
3068 	* Some widgets do not participate in placement, e.g. [StaticPosition], and some layout systems do their own separate thing too; ultimately, these properties are just hints to the layout function and you can always implement your own to do whatever you want. But this statement is still mostly true.
3069 +/
3070 mixin template Padding(string code) {
3071 	override int paddingLeft() { return mixin(code);}
3072 	override int paddingRight() { return mixin(code);}
3073 	override int paddingTop() { return mixin(code);}
3074 	override int paddingBottom() { return mixin(code);}
3075 }
3076 
3077 /// ditto
3078 mixin template Margin(string code) {
3079 	override int marginLeft() { return mixin(code);}
3080 	override int marginRight() { return mixin(code);}
3081 	override int marginTop() { return mixin(code);}
3082 	override int marginBottom() { return mixin(code);}
3083 }
3084 
3085 private
3086 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3087 	enum calcingV = relevantMeasure == "height";
3088 
3089 	parent.registerMovement();
3090 
3091 	if(parent.children.length == 0)
3092 		return;
3093 
3094 	auto parentStyle = parent.getComputedStyle();
3095 
3096 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3097 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3098 
3099 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3100 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3101 
3102 	// my own width and height should already be set by the caller of this function...
3103 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3104 		mixin("parentStyle.padding"~firstThingy~"()") -
3105 		mixin("parentStyle.padding"~secondThingy~"()");
3106 
3107 	int stretchinessSum;
3108 	int stretchyChildSum;
3109 	int lastMargin = 0;
3110 
3111 	int shrinkinessSum;
3112 	int shrinkyChildSum;
3113 
3114 	// set initial size
3115 	foreach(child; parent.children) {
3116 
3117 		auto childStyle = child.getComputedStyle();
3118 
3119 		if(cast(StaticPosition) child)
3120 			continue;
3121 		if(child.hidden)
3122 			continue;
3123 
3124 		const iw = child.flexBasisWidth();
3125 		const ih = child.flexBasisHeight();
3126 
3127 		static if(calcingV) {
3128 			child.width = parent.width -
3129 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3130 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3131 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3132 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3133 
3134 			if(child.width < 0)
3135 				child.width = 0;
3136 			if(child.width > childStyle.maxWidth())
3137 				child.width = childStyle.maxWidth();
3138 
3139 			if(iw > 0) {
3140 				auto totalPossible = child.width;
3141 				if(child.width > iw && child.widthStretchiness() == 0)
3142 					child.width = iw;
3143 			}
3144 
3145 			child.height = mymax(childStyle.minHeight(), ih);
3146 		} else {
3147 			// set to take all the space
3148 			child.height = parent.height -
3149 				mixin("childStyle.margin"~firstThingy~"()") -
3150 				mixin("childStyle.margin"~secondThingy~"()") -
3151 				mixin("parentStyle.padding"~firstThingy~"()") -
3152 				mixin("parentStyle.padding"~secondThingy~"()");
3153 
3154 			// then clamp it
3155 			if(child.height < 0)
3156 				child.height = 0;
3157 			if(child.height > childStyle.maxHeight())
3158 				child.height = childStyle.maxHeight();
3159 
3160 			// and if possible, respect the ideal target
3161 			if(ih > 0) {
3162 				auto totalPossible = child.height;
3163 				if(child.height > ih && child.heightStretchiness() == 0)
3164 					child.height = ih;
3165 			}
3166 
3167 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3168 			child.width = mymax(childStyle.minWidth(), iw);
3169 		}
3170 
3171 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3172 
3173 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3174 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3175 		lastMargin = margin;
3176 		spaceRemaining -= thisMargin + margin;
3177 
3178 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3179 		stretchinessSum += s;
3180 		if(s > 0)
3181 			stretchyChildSum++;
3182 
3183 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3184 		shrinkinessSum += s2;
3185 		if(s2 > 0)
3186 			shrinkyChildSum++;
3187 	}
3188 
3189 	if(spaceRemaining < 0 && shrinkyChildSum) {
3190 		// shrink to get into the space if it is possible
3191 		auto toRemove = -spaceRemaining;
3192 		auto removalPerItem  = toRemove * shrinkinessSum / shrinkyChildSum;
3193 		auto remainder = toRemove * shrinkinessSum % shrinkyChildSum;
3194 
3195 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3196 
3197 		foreach(child; parent.children) {
3198 			auto childStyle = child.getComputedStyle();
3199 			if(cast(StaticPosition) child)
3200 				continue;
3201 			if(child.hidden)
3202 				continue;
3203 			static if(calcingV) {
3204 				auto maximum = childStyle.maxHeight();
3205 			} else {
3206 				auto maximum = childStyle.maxWidth();
3207 			}
3208 
3209 			if(mixin("child._" ~ relevantMeasure) >= maximum)
3210 				continue;
3211 
3212 			mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh.
3213 
3214 			spaceRemaining += removalPerItem + remainder;
3215 		}
3216 	}
3217 
3218 	// stretch to fill space
3219 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3220 		auto spacePerChild = spaceRemaining / stretchinessSum;
3221 		bool spreadEvenly;
3222 		bool giveToBiggest;
3223 		if(spacePerChild <= 0) {
3224 			spacePerChild = spaceRemaining / stretchyChildSum;
3225 			spreadEvenly = true;
3226 		}
3227 		if(spacePerChild <= 0) {
3228 			giveToBiggest = true;
3229 		}
3230 		int previousSpaceRemaining = spaceRemaining;
3231 		stretchinessSum = 0;
3232 		Widget mostStretchy;
3233 		int mostStretchyS;
3234 		foreach(child; parent.children) {
3235 			auto childStyle = child.getComputedStyle();
3236 			if(cast(StaticPosition) child)
3237 				continue;
3238 			if(child.hidden)
3239 				continue;
3240 			static if(calcingV) {
3241 				auto maximum = childStyle.maxHeight();
3242 			} else {
3243 				auto maximum = childStyle.maxWidth();
3244 			}
3245 
3246 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3247 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3248 				mixin("child._" ~ relevantMeasure) -= adj;
3249 				spaceRemaining += adj;
3250 				continue;
3251 			}
3252 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3253 			if(s <= 0)
3254 				continue;
3255 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3256 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3257 			spaceRemaining -= spaceAdjustment;
3258 			if(mixin("child." ~ relevantMeasure) > maximum) {
3259 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3260 				mixin("child._" ~ relevantMeasure) -= diff;
3261 				spaceRemaining += diff;
3262 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3263 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3264 				if(mostStretchy is null || s >= mostStretchyS) {
3265 					mostStretchy = child;
3266 					mostStretchyS = s;
3267 				}
3268 			}
3269 		}
3270 
3271 		if(giveToBiggest && mostStretchy !is null) {
3272 			auto child = mostStretchy;
3273 			auto childStyle = child.getComputedStyle();
3274 			int spaceAdjustment = spaceRemaining;
3275 
3276 			static if(calcingV)
3277 				auto maximum = childStyle.maxHeight();
3278 			else
3279 				auto maximum = childStyle.maxWidth();
3280 
3281 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3282 			spaceRemaining -= spaceAdjustment;
3283 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3284 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3285 				mixin("child._" ~ relevantMeasure) -= diff;
3286 				spaceRemaining += diff;
3287 			}
3288 		}
3289 
3290 		if(spaceRemaining == previousSpaceRemaining) {
3291 			if(mostStretchy !is null) {
3292 				static if(calcingV)
3293 					auto maximum = mostStretchy.maxHeight();
3294 				else
3295 					auto maximum = mostStretchy.maxWidth();
3296 
3297 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3298 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3299 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3300 			}
3301 			break; // apparently nothing more we can do
3302 		}
3303 	}
3304 
3305 	foreach(child; parent.children) {
3306 		auto childStyle = child.getComputedStyle();
3307 		if(cast(StaticPosition) child)
3308 			continue;
3309 		if(child.hidden)
3310 			continue;
3311 
3312 		static if(calcingV)
3313 			auto maximum = childStyle.maxHeight();
3314 		else
3315 			auto maximum = childStyle.maxWidth();
3316 		if(mixin("child._" ~ relevantMeasure) > maximum)
3317 			mixin("child._" ~ relevantMeasure) = maximum;
3318 	}
3319 
3320 	// position
3321 	lastMargin = 0;
3322 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3323 	foreach(child; parent.children) {
3324 		auto childStyle = child.getComputedStyle();
3325 		if(cast(StaticPosition) child) {
3326 			child.recomputeChildLayout();
3327 			continue;
3328 		}
3329 		if(child.hidden)
3330 			continue;
3331 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3332 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3333 		currentPos += thisMargin;
3334 		static if(calcingV) {
3335 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3336 			child.y = currentPos;
3337 		} else {
3338 			child.x = currentPos;
3339 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3340 
3341 		}
3342 		currentPos += mixin("child." ~ relevantMeasure);
3343 		currentPos += margin;
3344 		lastMargin = margin;
3345 
3346 		child.recomputeChildLayout();
3347 	}
3348 }
3349 
3350 int mymax(int a, int b) { return a > b ? a : b; }
3351 int mymax(int a, int b, int c) {
3352 	auto d = mymax(a, b);
3353 	return c > d ? c : d;
3354 }
3355 
3356 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3357 // and here, it must be integrable with the layout, the event system, and not be painted over.
3358 version(win32_widgets) {
3359 
3360 	// this function just does stuff that a parent window needs for redirection
3361 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3362 		this_.hookedWndProc(msg, wParam, lParam);
3363 
3364 		switch(msg) {
3365 
3366 			case WM_VSCROLL, WM_HSCROLL:
3367 				auto pos = HIWORD(wParam);
3368 				auto m = LOWORD(wParam);
3369 
3370 				auto scrollbarHwnd = cast(HWND) lParam;
3371 
3372 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3373 
3374 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3375 
3376 					switch(m) {
3377 						/+
3378 						// I don't think those messages are ever actually sent normally by the widget itself,
3379 						// they are more used for the keyboard interface. methinks.
3380 						case SB_BOTTOM:
3381 							// writeln("end");
3382 							auto event = new Event("scrolltoend", *widgetp);
3383 							event.dispatch();
3384 							//if(!event.defaultPrevented)
3385 						break;
3386 						case SB_TOP:
3387 							// writeln("top");
3388 							auto event = new Event("scrolltobeginning", *widgetp);
3389 							event.dispatch();
3390 						break;
3391 						case SB_ENDSCROLL:
3392 							// idk
3393 						break;
3394 						+/
3395 						case SB_LINEDOWN:
3396 							(*widgetp).emitCommand!"scrolltonextline"();
3397 						return 0;
3398 						case SB_LINEUP:
3399 							(*widgetp).emitCommand!"scrolltopreviousline"();
3400 						return 0;
3401 						case SB_PAGEDOWN:
3402 							(*widgetp).emitCommand!"scrolltonextpage"();
3403 						return 0;
3404 						case SB_PAGEUP:
3405 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3406 						return 0;
3407 						case SB_THUMBPOSITION:
3408 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3409 							ev.dispatch();
3410 						return 0;
3411 						case SB_THUMBTRACK:
3412 							// eh kinda lying but i like the real time update display
3413 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3414 							ev.dispatch();
3415 
3416 							// the event loop doesn't seem to carry on with a requested redraw..
3417 							// so we request it to get our dirty bit set...
3418 							// then we need to immediately actually redraw it too for instant feedback to user
3419 							SimpleWindow.processAllCustomEvents();
3420 							SimpleWindow.processAllCustomEvents();
3421 							//if(this_.parentWindow)
3422 								//this_.parentWindow.actualRedraw();
3423 
3424 							// and this ensures the WM_PAINT message is sent fairly quickly
3425 							// still seems to lag a little in large windows but meh it basically works.
3426 							if(this_.parentWindow) {
3427 								// FIXME: if painting is slow, this does still lag
3428 								// we probably will want to expose some user hook to ScrollWindowEx
3429 								// or something.
3430 								UpdateWindow(this_.parentWindow.hwnd);
3431 							}
3432 						return 0;
3433 						default:
3434 					}
3435 				}
3436 			break;
3437 
3438 			case WM_CONTEXTMENU:
3439 				auto hwndFrom = cast(HWND) wParam;
3440 
3441 				auto xPos = cast(short) LOWORD(lParam);
3442 				auto yPos = cast(short) HIWORD(lParam);
3443 
3444 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3445 					POINT p;
3446 					p.x = xPos;
3447 					p.y = yPos;
3448 					ScreenToClient(hwnd, &p);
3449 					auto clientX = cast(ushort) p.x;
3450 					auto clientY = cast(ushort) p.y;
3451 
3452 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
3453 
3454 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
3455 						return 0;
3456 					}
3457 				}
3458 			break;
3459 
3460 			case WM_DRAWITEM:
3461 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
3462 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
3463 					return (*widgetp).handleWmDrawItem(dis);
3464 				}
3465 			break;
3466 
3467 			case WM_NOTIFY:
3468 				auto hdr = cast(NMHDR*) lParam;
3469 				auto hwndFrom = hdr.hwndFrom;
3470 				auto code = hdr.code;
3471 
3472 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3473 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
3474 				}
3475 			break;
3476 			case WM_COMMAND:
3477 				auto handle = cast(HWND) lParam;
3478 				auto cmd = HIWORD(wParam);
3479 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
3480 
3481 			default:
3482 				// pass it on
3483 		}
3484 		return 0;
3485 	}
3486 
3487 
3488 
3489 	extern(Windows)
3490 	private
3491 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
3492 	// but can i merge them?!
3493 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3494 		// try { writeln(iMessage); } catch(Exception e) {};
3495 
3496 		if(auto te = hWnd in Widget.nativeMapping) {
3497 			try {
3498 
3499 				te.hookedWndProc(iMessage, wParam, lParam);
3500 
3501 				int mustReturn;
3502 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
3503 				if(mustReturn)
3504 					return ret;
3505 
3506 				if(iMessage == WM_SETFOCUS) {
3507 					auto lol = *te;
3508 					while(lol !is null && lol.implicitlyCreated)
3509 						lol = lol.parent;
3510 					lol.focus();
3511 					//(*te).parentWindow.focusedWidget = lol;
3512 				}
3513 
3514 
3515 				if(iMessage == WM_CTLCOLOREDIT) {
3516 
3517 				}
3518 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
3519 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
3520 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
3521 						//GetStockObject(NULL_BRUSH);
3522 				}
3523 
3524 				auto pos = getChildPositionRelativeToParentOrigin(*te);
3525 				lastDefaultPrevented = false;
3526 				// try { writeln(typeid(*te)); } catch(Exception e) {}
3527 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
3528 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
3529 				else {
3530 					// it was something we recognized, should only call the window procedure if the default was not prevented
3531 				}
3532 			} catch(Exception e) {
3533 				assert(0, e.toString());
3534 			}
3535 			return 0;
3536 		}
3537 		assert(0, "shouldn't be receiving messages for this window....");
3538 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
3539 	}
3540 
3541 	extern(Windows)
3542 	private
3543 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
3544 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3545 		if(iMessage == WM_ERASEBKGND) {
3546 			auto dc = GetDC(hWnd);
3547 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
3548 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
3549 			RECT r;
3550 			GetWindowRect(hWnd, &r);
3551 			// since the pen is null, to fill the whole space, we need the +1 on both.
3552 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
3553 			SelectObject(dc, p);
3554 			SelectObject(dc, b);
3555 			ReleaseDC(hWnd, dc);
3556 			InvalidateRect(hWnd, null, false); // redraw the border
3557 			return 1;
3558 		}
3559 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
3560 	}
3561 
3562 	/++
3563 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
3564 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
3565 		of minigui's expectations.
3566 
3567 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
3568 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
3569 
3570 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
3571 
3572 		To check if you can use this, use `static if(UsingWin32Widgets)`.
3573 	+/
3574 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
3575 		assert(p.parentWindow !is null);
3576 		assert(p.parentWindow.win.impl.hwnd !is null);
3577 
3578 		auto bsgroupbox = style == BS_GROUPBOX;
3579 
3580 		HWND phwnd;
3581 
3582 		auto wtf = p.parent;
3583 		while(wtf) {
3584 			if(wtf.hwnd !is null) {
3585 				phwnd = wtf.hwnd;
3586 				break;
3587 			}
3588 			wtf = wtf.parent;
3589 		}
3590 
3591 		if(phwnd is null)
3592 			phwnd = p.parentWindow.win.impl.hwnd;
3593 
3594 		assert(phwnd !is null);
3595 
3596 		WCharzBuffer wt = WCharzBuffer(windowText);
3597 
3598 		style |= WS_VISIBLE | WS_CHILD;
3599 		//if(className != WC_TABCONTROL)
3600 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
3601 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
3602 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
3603 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
3604 
3605 		assert(p.hwnd !is null);
3606 
3607 
3608 		static HFONT font;
3609 		if(font is null) {
3610 			NONCLIENTMETRICS params;
3611 			params.cbSize = params.sizeof;
3612 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
3613 				font = CreateFontIndirect(&params.lfMessageFont);
3614 			}
3615 		}
3616 
3617 		if(font)
3618 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
3619 
3620 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
3621 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
3622 		Widget.nativeMapping[p.hwnd] = p;
3623 
3624 		if(bsgroupbox)
3625 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
3626 		else
3627 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3628 
3629 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
3630 
3631 		p.registerMovement();
3632 	}
3633 }
3634 
3635 version(win32_widgets)
3636 private
3637 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
3638 	if(hwnd is null || hwnd in Widget.nativeMapping)
3639 		return true;
3640 	auto parent = cast(Widget) cast(void*) lparam;
3641 	Widget p = new Widget(null);
3642 	p._parent = parent;
3643 	p.parentWindow = parent.parentWindow;
3644 	p.hwnd = hwnd;
3645 	p.implicitlyCreated = true;
3646 	Widget.nativeMapping[p.hwnd] = p;
3647 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3648 	return true;
3649 }
3650 
3651 /++
3652 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
3653 +/
3654 struct WidgetPainter {
3655 	this(ScreenPainter screenPainter, Widget drawingUpon) {
3656 		this.drawingUpon = drawingUpon;
3657 		this.screenPainter = screenPainter;
3658 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3659 			this.screenPainter.setFont(font);
3660 	}
3661 
3662 	/++
3663 		EXPERIMENTAL. subject to change.
3664 
3665 		When you draw a cursor, you can draw this to notify your window of where it is,
3666 		for IME systems to use.
3667 	+/
3668 	void notifyCursorPosition(int x, int y, int width, int height) {
3669 		if(auto a = drawingUpon.parentWindow)
3670 		if(auto w = a.inputProxy) {
3671 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
3672 		}
3673 	}
3674 
3675 
3676 	///
3677 	ScreenPainter screenPainter;
3678 	/// Forward to the screen painter for other methods
3679 	alias screenPainter this;
3680 
3681 	private Widget drawingUpon;
3682 
3683 	/++
3684 		This is the list of rectangles that actually need to be redrawn.
3685 
3686 		Not actually implemented yet.
3687 	+/
3688 	Rectangle[] invalidatedRectangles;
3689 
3690 	private static BaseVisualTheme _visualTheme;
3691 
3692 	/++
3693 		Functions to access the visual theme and helpers to easily use it.
3694 
3695 		These are aware of the current widget's computed style out of the theme.
3696 	+/
3697 	static @property BaseVisualTheme visualTheme() {
3698 		if(_visualTheme is null)
3699 			_visualTheme = new DefaultVisualTheme();
3700 		return _visualTheme;
3701 	}
3702 
3703 	/// ditto
3704 	static @property void visualTheme(BaseVisualTheme theme) {
3705 		_visualTheme = theme;
3706 
3707 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
3708 	}
3709 
3710 	/// ditto
3711 	Color themeForeground() {
3712 		return drawingUpon.getComputedStyle().foregroundColor();
3713 	}
3714 
3715 	/// ditto
3716 	Color themeBackground() {
3717 		return drawingUpon.getComputedStyle().background.color;
3718 	}
3719 
3720 	int isDarkTheme() {
3721 		return 0; // unspecified, yes, no as enum. FIXME
3722 	}
3723 
3724 	/++
3725 		Draws the general pattern of a widget if you don't need anything particularly special and/or control the other details through your widget's style theme hints.
3726 
3727 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
3728 
3729 		If you change teh clip rectangle, you should change it back before you return.
3730 
3731 
3732 		The sequence it uses is:
3733 			background
3734 			content (delegated to you)
3735 			border
3736 			focused outline
3737 			selected overlay
3738 
3739 		Example code:
3740 
3741 		---
3742 		void paint(WidgetPainter painter) {
3743 			painter.drawThemed((bounds) {
3744 				return bounds; // if the selection overlay should be contained, you can return it here.
3745 			});
3746 		}
3747 		---
3748 	+/
3749 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
3750 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
3751 			return drawBody(bounds);
3752 		});
3753 	}
3754 	// this overload is actually mroe for setting the delegate to a virtual function
3755 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
3756 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
3757 
3758 		auto cs = drawingUpon.getComputedStyle();
3759 
3760 		auto bg = cs.background.color;
3761 
3762 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
3763 
3764 		rect.left += borderWidth;
3765 		rect.right -= borderWidth;
3766 		rect.top += borderWidth;
3767 		rect.bottom -= borderWidth;
3768 
3769 		auto insideBorderRect = rect;
3770 
3771 		rect.left += cs.paddingLeft;
3772 		rect.right -= cs.paddingRight;
3773 		rect.top += cs.paddingTop;
3774 		rect.bottom -= cs.paddingBottom;
3775 
3776 		this.outlineColor = this.themeForeground;
3777 		this.fillColor = bg;
3778 
3779 		auto widgetFont = cs.fontCached;
3780 		if(widgetFont !is null)
3781 			this.setFont(widgetFont);
3782 
3783 		rect = drawBody(this, rect);
3784 
3785 		if(widgetFont !is null) {
3786 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3787 				this.setFont(vtFont);
3788 			else
3789 				this.setFont(null);
3790 		}
3791 
3792 		if(auto os = cs.outlineStyle()) {
3793 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
3794 			this.fillColor = Color.transparent;
3795 			this.drawRectangle(insideBorderRect);
3796 		}
3797 	}
3798 
3799 	/++
3800 		First, draw the background.
3801 		Then draw your content.
3802 		Next, draw the border.
3803 		And the focused indicator.
3804 		And the is-selected box.
3805 
3806 		If it is focused i can draw the outline too...
3807 
3808 		If selected i can even do the xor action but that's at the end.
3809 	+/
3810 	void drawThemeBackground() {
3811 
3812 	}
3813 
3814 	void drawThemeBorder() {
3815 
3816 	}
3817 
3818 	// all this stuff is a dangerous experiment....
3819 	static class ScriptableVersion {
3820 		ScreenPainterImplementation* p;
3821 		int originX, originY;
3822 
3823 		@scriptable:
3824 		void drawRectangle(int x, int y, int width, int height) {
3825 			p.drawRectangle(x + originX, y + originY, width, height);
3826 		}
3827 		void drawLine(int x1, int y1, int x2, int y2) {
3828 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
3829 		}
3830 		void drawText(int x, int y, string text) {
3831 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
3832 		}
3833 		void setOutlineColor(int r, int g, int b) {
3834 			p.pen = Pen(Color(r,g,b), 1);
3835 		}
3836 		void setFillColor(int r, int g, int b) {
3837 			p.fillColor = Color(r,g,b);
3838 		}
3839 	}
3840 
3841 	ScriptableVersion toArsdJsvar() {
3842 		auto sv = new ScriptableVersion;
3843 		sv.p = this.screenPainter.impl;
3844 		sv.originX = this.screenPainter.originX;
3845 		sv.originY = this.screenPainter.originY;
3846 		return sv;
3847 	}
3848 
3849 	static WidgetPainter fromJsVar(T)(T t) {
3850 		return WidgetPainter.init;
3851 	}
3852 	// done..........
3853 }
3854 
3855 
3856 struct Style {
3857 	static struct helper(string m, T) {
3858 		enum method = m;
3859 		T v;
3860 
3861 		mixin template MethodOverride(typeof(this) v) {
3862 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
3863 		}
3864 	}
3865 
3866 	static auto opDispatch(string method, T)(T value) {
3867 		return helper!(method, T)(value);
3868 	}
3869 }
3870 
3871 /++
3872 	Implementation detail of the [ControlledBy] UDA.
3873 
3874 	History:
3875 		Added Oct 28, 2020
3876 +/
3877 struct ControlledBy_(T, Args...) {
3878 	Args args;
3879 
3880 	static if(Args.length)
3881 	this(Args args) {
3882 		this.args = args;
3883 	}
3884 
3885 	private T construct(Widget parent) {
3886 		return new T(args, parent);
3887 	}
3888 }
3889 
3890 /++
3891 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
3892 
3893 	History:
3894 		Added Oct 28, 2020
3895 +/
3896 auto ControlledBy(T, Args...)(Args args) {
3897 	return ControlledBy_!(T, Args)(args);
3898 }
3899 
3900 struct ContainerMeta {
3901 	string name;
3902 	ContainerMeta[] children;
3903 	Widget function(Widget parent) factory;
3904 
3905 	Widget instantiate(Widget parent) {
3906 		auto n = factory(parent);
3907 		n.name = name;
3908 		foreach(child; children)
3909 			child.instantiate(n);
3910 		return n;
3911 	}
3912 }
3913 
3914 /++
3915 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
3916 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
3917 
3918 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
3919 	structures. It works fine on structs declared inside functions though.
3920 
3921 	See: https://issues.dlang.org/show_bug.cgi?id=21984
3922 +/
3923 template Container(CArgs...) {
3924 	static if(CArgs.length && is(CArgs[0] : Widget)) {
3925 		private alias Super = CArgs[0];
3926 		private alias CArgs2 = CArgs[1 .. $];
3927 	} else {
3928 		private alias Super = Layout;
3929 		private alias CArgs2 = CArgs;
3930 	}
3931 
3932 	class Container : Super {
3933 		this(Widget parent) { super(parent); }
3934 
3935 		// just to partially support old gdc versions
3936 		version(GNU) {
3937 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
3938 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
3939 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
3940 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
3941 		} else mixin(q{
3942 			static foreach(Arg; CArgs2) {
3943 				mixin Arg.MethodOverride!(Arg);
3944 			}
3945 		});
3946 
3947 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
3948 			return ContainerMeta(
3949 				name,
3950 				children.dup,
3951 				function (Widget parent) { return new typeof(this)(parent); }
3952 			);
3953 		}
3954 
3955 		static ContainerMeta opCall(ContainerMeta[] children...) {
3956 			return opCall(null, children);
3957 		}
3958 	}
3959 }
3960 
3961 /++
3962 	The data controller widget is created by reflecting over the given
3963 	data type. You can use [ControlledBy] as a UDA on a struct or
3964 	just let it create things automatically.
3965 
3966 	Unlike [dialog], this uses real-time updating of the data and
3967 	you add it to another window yourself.
3968 
3969 	---
3970 		struct Test {
3971 			int x;
3972 			int y;
3973 		}
3974 
3975 		auto window = new Window();
3976 		auto dcw = new DataControllerWidget!Test(new Test, window);
3977 	---
3978 
3979 	The way it works is any public members are given a widget based
3980 	on their data type, and public methods trigger an action button
3981 	if no relevant parameters or a dialog action if it does have
3982 	parameters, similar to the [menu] facility.
3983 
3984 	If you change data programmatically, without going through the
3985 	DataControllerWidget methods, you will have to tell it something
3986 	has changed and it needs to redraw. This is done with the `invalidate`
3987 	method.
3988 
3989 	History:
3990 		Added Oct 28, 2020
3991 +/
3992 /// Group: generating_from_code
3993 class DataControllerWidget(T) : WidgetContainer {
3994 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
3995 		private alias Tref = T;
3996 	else
3997 		private alias Tref = T*;
3998 
3999 	Tref datum;
4000 
4001 	/++
4002 		See_also: [addDataControllerWidget]
4003 	+/
4004 	this(Tref datum, Widget parent) {
4005 		this.datum = datum;
4006 
4007 		Widget cp = this;
4008 
4009 		super(parent);
4010 
4011 		foreach(attr; __traits(getAttributes, T))
4012 			static if(is(typeof(attr) == ContainerMeta)) {
4013 				cp = attr.instantiate(this);
4014 			}
4015 
4016 		auto def = this.getByName("default");
4017 		if(def !is null)
4018 			cp = def;
4019 
4020 		Widget helper(string name) {
4021 			auto maybe = this.getByName(name);
4022 			if(maybe is null)
4023 				return cp;
4024 			return maybe;
4025 
4026 		}
4027 
4028 		foreach(member; __traits(allMembers, T))
4029 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4030 		static if(is(typeof(__traits(getMember, this.datum, member))))
4031 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4032 			void delegate() update;
4033 
4034 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4035 
4036 			if(update)
4037 				updaters ~= update;
4038 
4039 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4040 				w.addEventListener("triggered", delegate() {
4041 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))();
4042 					notifyDataUpdated();
4043 				});
4044 			} else static if(is(typeof(w.isChecked) == bool)) {
4045 				w.addEventListener(EventType.change, (Event ev) {
4046 					__traits(getMember, this.datum, member) = w.isChecked;
4047 				});
4048 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4049 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4050 			} else static if(is(typeof(w.value) == int)) {
4051 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4052 			} else static if(is(typeof(w) == DropDownSelection)) {
4053 				// special case for this to kinda support enums and such. coudl be better though
4054 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4055 			} else {
4056 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4057 			}
4058 		}
4059 	}
4060 
4061 	/++
4062 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4063 
4064 		History:
4065 			Added May 28, 2021
4066 	+/
4067 	void notifyDataUpdated() {
4068 		foreach(updater; updaters)
4069 			updater();
4070 
4071 		this.emit!(ChangeEvent!void)(delegate{});
4072 	}
4073 
4074 	private Widget[string] memberWidgets;
4075 	private void delegate()[] updaters;
4076 
4077 	mixin Emits!(ChangeEvent!void);
4078 }
4079 
4080 private int saturatedSum(int[] values...) {
4081 	int sum;
4082 	foreach(value; values) {
4083 		if(value == int.max)
4084 			return int.max;
4085 		sum += value;
4086 	}
4087 	return sum;
4088 }
4089 
4090 void genericSetValue(T, W)(T* where, W what) {
4091 	import std.conv;
4092 	*where = to!T(what);
4093 	//*where = cast(T) stringToLong(what);
4094 }
4095 
4096 /++
4097 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4098 
4099 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4100 
4101 	Note that this creates the widget but does not attach any event handlers to it.
4102 +/
4103 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4104 
4105 	string displayName = __traits(identifier, tt).beautify;
4106 
4107 	static if(controlledByCount!tt == 1) {
4108 		foreach(i, attr; __traits(getAttributes, tt)) {
4109 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4110 				auto w = attr.construct(parent);
4111 				static if(__traits(compiles, w.setPosition(*valptr)))
4112 					update = () { w.setPosition(*valptr); };
4113 				else static if(__traits(compiles, w.setValue(*valptr)))
4114 					update = () { w.setValue(*valptr); };
4115 
4116 				if(update)
4117 					update();
4118 				return w;
4119 			}
4120 		}
4121 	} else static if(controlledByCount!tt == 0) {
4122 		static if(is(typeof(tt) == enum)) {
4123 			// FIXME: update
4124 			auto dds = new DropDownSelection(parent);
4125 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4126 				dds.addOption(option);
4127 				if(__traits(getMember, typeof(tt), option) == *valptr)
4128 					dds.setSelection(cast(int) idx);
4129 			}
4130 			return dds;
4131 		} else static if(is(typeof(tt) == bool)) {
4132 			auto box = new Checkbox(displayName, parent);
4133 			update = () { box.isChecked = *valptr; };
4134 			update();
4135 			return box;
4136 		} else static if(is(typeof(tt) : const long)) {
4137 			auto le = new LabeledLineEdit(displayName, parent);
4138 			update = () { le.content = toInternal!string(*valptr); };
4139 			update();
4140 			return le;
4141 		} else static if(is(typeof(tt) : const double)) {
4142 			auto le = new LabeledLineEdit(displayName, parent);
4143 			import std.conv;
4144 			update = () { le.content = to!string(*valptr); };
4145 			update();
4146 			return le;
4147 		} else static if(is(typeof(tt) : const string)) {
4148 			auto le = new LabeledLineEdit(displayName, parent);
4149 			update = () { le.content = *valptr; };
4150 			update();
4151 			return le;
4152 		} else static if(is(typeof(tt) == function)) {
4153 			auto w = new Button(displayName, parent);
4154 			return w;
4155 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4156 			return parent.addDataControllerWidget(tt);
4157 		} else static assert(0, typeof(tt).stringof);
4158 	} else static assert(0, "multiple controllers not yet supported");
4159 }
4160 
4161 private template controlledByCount(alias tt) {
4162 	static int helper() {
4163 		int count;
4164 		foreach(i, attr; __traits(getAttributes, tt))
4165 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4166 				count++;
4167 		return count;
4168 	}
4169 
4170 	enum controlledByCount = helper;
4171 }
4172 
4173 /++
4174 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4175 
4176 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4177 
4178 	History:
4179 		The `redrawOnChange` parameter was added on May 28, 2021.
4180 +/
4181 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4182 	auto dcw = new DataControllerWidget!T(t, parent);
4183 	initializeDataControllerWidget(dcw, redrawOnChange);
4184 	return dcw;
4185 }
4186 
4187 /// ditto
4188 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4189 	auto dcw = new DataControllerWidget!T(t, parent);
4190 	initializeDataControllerWidget(dcw, redrawOnChange);
4191 	return dcw;
4192 }
4193 
4194 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4195 	if(redrawOnChange !is null)
4196 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4197 }
4198 
4199 /++
4200 	Get this through [Widget.getComputedStyle]. It provides access to the [Widget.Style] style hints and [Widget] layout hints, possibly modified through the [VisualTheme], through a unifed interface.
4201 
4202 	History:
4203 		Finalized on June 3, 2021 for the dub v10.0 release
4204 +/
4205 struct StyleInformation {
4206 	private Widget w;
4207 	private BaseVisualTheme visualTheme;
4208 
4209 	private this(Widget w) {
4210 		this.w = w;
4211 		this.visualTheme = WidgetPainter.visualTheme;
4212 	}
4213 
4214 	/++
4215 		Forwards to [Widget.Style]
4216 
4217 		Bugs:
4218 			It is supposed to fall back to the [VisualTheme] if
4219 			the style doesn't override the default, but that is
4220 			not generally implemented. Many of them may end up
4221 			being explicit overloads instead of the generic
4222 			opDispatch fallback, like [font] is now.
4223 	+/
4224 	public @property opDispatch(string name)() {
4225 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4226 		w.useStyleProperties((scope Widget.Style props) {
4227 		//visualTheme.useStyleProperties(w, (props) {
4228 			prop = __traits(getMember, props, name);
4229 		});
4230 		return prop;
4231 	}
4232 
4233 	/++
4234 		Returns the cached font object associated with the widget,
4235 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4236 
4237 		History:
4238 			Prior to March 21, 2022 (dub v10.7), `font` went through
4239 			[opDispatch], which did not use the cache. You can now call it
4240 			repeatedly without guilt.
4241 	+/
4242 	public @property OperatingSystemFont font() {
4243 		OperatingSystemFont prop;
4244 		w.useStyleProperties((scope Widget.Style props) {
4245 			prop = props.fontCached;
4246 		});
4247 		if(prop is null) {
4248 			prop = visualTheme.defaultFontCached(w.currentDpi);
4249 		}
4250 		return prop;
4251 	}
4252 
4253 	@property {
4254 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4255 		/** */ int paddingLeft() { return w.paddingLeft(); }
4256 		/** */ int paddingRight() { return w.paddingRight(); }
4257 		/** */ int paddingTop() { return w.paddingTop(); }
4258 		/** */ int paddingBottom() { return w.paddingBottom(); }
4259 
4260 		/** */ int marginLeft() { return w.marginLeft(); }
4261 		/** */ int marginRight() { return w.marginRight(); }
4262 		/** */ int marginTop() { return w.marginTop(); }
4263 		/** */ int marginBottom() { return w.marginBottom(); }
4264 
4265 		/** */ int maxHeight() { return w.maxHeight(); }
4266 		/** */ int minHeight() { return w.minHeight(); }
4267 
4268 		/** */ int maxWidth() { return w.maxWidth(); }
4269 		/** */ int minWidth() { return w.minWidth(); }
4270 
4271 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4272 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4273 
4274 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4275 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4276 
4277 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4278 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4279 
4280 		// Global helpers some of these are unstable.
4281 		static:
4282 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4283 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4284 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4285 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4286 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4287 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4288 
4289 		/** */ Color activeTabColor() { return lightAccentColor; }
4290 		/** */ Color buttonColor() { return windowBackgroundColor; }
4291 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4292 		/** */ Color hoveringColor() { return lightAccentColor; }
4293 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4294 			auto c = WidgetPainter.visualTheme.selectionColor();
4295 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4296 		}
4297 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4298 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4299 	}
4300 
4301 
4302 
4303 	/+
4304 
4305 	private static auto extractStyleProperty(string name)(Widget w) {
4306 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4307 		w.useStyleProperties((props) {
4308 			prop = __traits(getMember, props, name);
4309 		});
4310 		return prop;
4311 	}
4312 
4313 	// FIXME: clear this upon a X server disconnect
4314 	private static OperatingSystemFont[string] fontCache;
4315 
4316 	T getProperty(T)(string name, lazy T default_) {
4317 		if(visualTheme !is null) {
4318 			auto str = visualTheme.getPropertyString(w, name);
4319 			if(str is null)
4320 				return default_;
4321 			static if(is(T == Color))
4322 				return Color.fromString(str);
4323 			else static if(is(T == Measurement))
4324 				return Measurement(cast(int) toInternal!int(str));
4325 			else static if(is(T == WidgetBackground))
4326 				return WidgetBackground.fromString(str);
4327 			else static if(is(T == OperatingSystemFont)) {
4328 				if(auto f = str in fontCache)
4329 					return *f;
4330 				else
4331 					return fontCache[str] = new OperatingSystemFont(str);
4332 			} else static if(is(T == FrameStyle)) {
4333 				switch(str) {
4334 					default:
4335 						return FrameStyle.none;
4336 					foreach(style; __traits(allMembers, FrameStyle))
4337 					case style:
4338 						return __traits(getMember, FrameStyle, style);
4339 				}
4340 			} else static assert(0);
4341 		} else
4342 			return default_;
4343 	}
4344 
4345 	static struct Measurement {
4346 		int value;
4347 		alias value this;
4348 	}
4349 
4350 	@property:
4351 
4352 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4353 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4354 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4355 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4356 
4357 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4358 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4359 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4360 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4361 
4362 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4363 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4364 
4365 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4366 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4367 
4368 
4369 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4370 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4371 
4372 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4373 
4374 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4375 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4376 
4377 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4378 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4379 
4380 
4381 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4382 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4383 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4384 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4385 
4386 	Color activeTabColor() { return lightAccentColor; }
4387 	Color buttonColor() { return windowBackgroundColor; }
4388 	Color depressedButtonColor() { return darkAccentColor; }
4389 	Color hoveringColor() { return Color(228, 228, 228); }
4390 	Color activeListXorColor() {
4391 		auto c = WidgetPainter.visualTheme.selectionColor();
4392 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4393 	}
4394 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4395 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4396 	+/
4397 }
4398 
4399 
4400 
4401 // pragma(msg, __traits(classInstanceSize, Widget));
4402 
4403 /*private*/ template EventString(E) {
4404 	static if(is(typeof(E.EventString)))
4405 		enum EventString = E.EventString;
4406 	else
4407 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4408 }
4409 
4410 /*private*/ template EventStringIdentifier(E) {
4411 	string helper() {
4412 		auto es = EventString!E;
4413 		char[] id = new char[](es.length * 2);
4414 		size_t idx;
4415 		foreach(char ch; es) {
4416 			id[idx++] = cast(char)('a' + (ch >> 4));
4417 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4418 		}
4419 		return cast(string) id;
4420 	}
4421 
4422 	enum EventStringIdentifier = helper();
4423 }
4424 
4425 
4426 template classStaticallyEmits(This, EventType) {
4427 	static if(is(This Base == super))
4428 		static if(is(Base : Widget))
4429 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4430 		else
4431 			enum baseEmits = false;
4432 	else
4433 		enum baseEmits = false;
4434 
4435 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
4436 
4437 	enum classStaticallyEmits = thisEmits || baseEmits;
4438 }
4439 
4440 /++
4441 	A helper to make widgets out of other native windows.
4442 
4443 	History:
4444 		Factored out of OpenGlWidget on November 5, 2021
4445 +/
4446 class NestedChildWindowWidget : Widget {
4447 	SimpleWindow win;
4448 
4449 	/++
4450 		Used on X to send focus to the appropriate child window when requested by the window manager.
4451 
4452 		Normally returns its own nested window. Can also return another child or null to revert to the parent
4453 		if you override it in a child class.
4454 
4455 		History:
4456 			Added April 2, 2022 (dub v10.8)
4457 	+/
4458 	SimpleWindow focusableWindow() {
4459 		return win;
4460 	}
4461 
4462 	///
4463 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4464 	this(SimpleWindow win, Widget parent) {
4465 		this.parentWindow = parent.parentWindow;
4466 		this.win = win;
4467 
4468 		super(parent);
4469 		windowsetup(win);
4470 	}
4471 
4472 	static protected SimpleWindow getParentWindow(Widget parent) {
4473 		assert(parent !is null);
4474 		SimpleWindow pwin = parent.parentWindow.win;
4475 
4476 		version(win32_widgets) {
4477 			HWND phwnd;
4478 			auto wtf = parent;
4479 			while(wtf) {
4480 				if(wtf.hwnd) {
4481 					phwnd = wtf.hwnd;
4482 					break;
4483 				}
4484 				wtf = wtf.parent;
4485 			}
4486 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
4487 			if(phwnd)
4488 				pwin = new SimpleWindow(phwnd);
4489 		}
4490 
4491 		return pwin;
4492 	}
4493 
4494 	/++
4495 		Called upon the nested window being destroyed.
4496 		Remember the window has already been destroyed at
4497 		this point, so don't use the native handle for anything.
4498 
4499 		History:
4500 			Added April 3, 2022 (dub v10.8)
4501 	+/
4502 	protected void dispose() {
4503 
4504 	}
4505 
4506 	protected void windowsetup(SimpleWindow w) {
4507 		/*
4508 		win.onFocusChange = (bool getting) {
4509 			if(getting)
4510 				this.focus();
4511 		};
4512 		*/
4513 
4514 		/+
4515 		win.onFocusChange = (bool getting) {
4516 			if(getting) {
4517 				this.parentWindow.focusedWidget = this;
4518 				this.emit!FocusEvent();
4519 				this.emit!FocusInEvent();
4520 			} else {
4521 				this.emit!BlurEvent();
4522 				this.emit!FocusOutEvent();
4523 			}
4524 		};
4525 		+/
4526 
4527 		win.onDestroyed = () {
4528 			this.dispose();
4529 		};
4530 
4531 		version(win32_widgets) {
4532 			Widget.nativeMapping[win.hwnd] = this;
4533 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4534 		} else {
4535 			win.setEventHandlers(
4536 				(MouseEvent e) {
4537 					Widget p = this;
4538 					while(p ! is parentWindow) {
4539 						e.x += p.x;
4540 						e.y += p.y;
4541 						p = p.parent;
4542 					}
4543 					parentWindow.dispatchMouseEvent(e);
4544 				},
4545 				(KeyEvent e) {
4546 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
4547 					parentWindow.dispatchKeyEvent(e);
4548 				},
4549 				(dchar e) {
4550 					parentWindow.dispatchCharEvent(e);
4551 				},
4552 			);
4553 		}
4554 
4555 	}
4556 
4557 	override void showing(bool s, bool recalc) {
4558 		auto cur = hidden;
4559 		win.hidden = !s;
4560 		if(cur != s && s)
4561 			redraw();
4562 	}
4563 
4564 	/// OpenGL widgets cannot have child widgets. Do not call this.
4565 	/* @disable */ final override void addChild(Widget, int) {
4566 		throw new Error("cannot add children to OpenGL widgets");
4567 	}
4568 
4569 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
4570 	/// Keep in mind that events like mouse coordinates are still relative to your size.
4571 	override void registerMovement() {
4572 		// writefln("%d %d %d %d", x,y,width,height);
4573 		version(win32_widgets)
4574 			auto pos = getChildPositionRelativeToParentHwnd(this);
4575 		else
4576 			auto pos = getChildPositionRelativeToParentOrigin(this);
4577 		win.moveResize(pos[0], pos[1], width, height);
4578 
4579 		registerMovementAdditionalWork();
4580 		sendResizeEvent();
4581 	}
4582 
4583 	abstract void registerMovementAdditionalWork();
4584 }
4585 
4586 /++
4587 	Nests an opengl capable window inside this window as a widget.
4588 
4589 	You may also just want to create an additional [SimpleWindow] with
4590 	[OpenGlOptions.yes] yourself.
4591 
4592 	An OpenGL widget cannot have child widgets. It will throw if you try.
4593 +/
4594 static if(OpenGlEnabled)
4595 class OpenGlWidget : NestedChildWindowWidget {
4596 
4597 	override void registerMovementAdditionalWork() {
4598 		win.setAsCurrentOpenGlContext();
4599 	}
4600 
4601 	///
4602 	this(Widget parent) {
4603 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4604 		super(win, parent);
4605 	}
4606 
4607 	override void paint(WidgetPainter painter) {
4608 		win.setAsCurrentOpenGlContext();
4609 		glViewport(0, 0, this.width, this.height);
4610 		win.redrawOpenGlSceneNow();
4611 	}
4612 
4613 	void redrawOpenGlScene(void delegate() dg) {
4614 		win.redrawOpenGlScene = dg;
4615 	}
4616 }
4617 
4618 /++
4619 	This demo shows how to draw text in an opengl scene.
4620 +/
4621 unittest {
4622 	import arsd.minigui;
4623 	import arsd.ttf;
4624 
4625 	void main() {
4626 		auto window = new Window();
4627 
4628 		auto widget = new OpenGlWidget(window);
4629 
4630 		// old means non-shader code so compatible with glBegin etc.
4631 		// tbh I haven't implemented new one in font yet...
4632 		// anyway, declaring here, will construct soon.
4633 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
4634 
4635 		// this is a little bit awkward, calling some methods through
4636 		// the underlying SimpleWindow `win` method, and you can't do this
4637 		// on a nanovega widget due to conflicts so I should probably fix
4638 		// the api to be a bit easier. But here it will work.
4639 		//
4640 		// Alternatively, you could load the font on the first draw, inside
4641 		// the redrawOpenGlScene, and keep a flag so you don't do it every
4642 		// time. That'd be a bit easier since the lib sets up the context
4643 		// by then guaranteed.
4644 		//
4645 		// But still, I wanna show this.
4646 		widget.win.visibleForTheFirstTime = delegate {
4647 			// must set the opengl context
4648 			widget.win.setAsCurrentOpenGlContext();
4649 
4650 			// if you were doing a OpenGL 3+ shader, this
4651 			// gets especially important to do in order. With
4652 			// old-style opengl, I think you can even do it
4653 			// in main(), but meh, let's show it more correctly.
4654 
4655 			// Anyway, now it is time to load the font from the
4656 			// OS (you can alternatively load one from a .ttf file
4657 			// you bundle with the application), then load the
4658 			// font into texture for drawing.
4659 
4660 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
4661 
4662 			assert(!osfont.isNull()); // make sure it actually loaded
4663 
4664 			// using typeof to avoid repeating the long name lol
4665 			glfont = new typeof(glfont)(
4666 				// get the raw data from the font for loading in here
4667 				// since it doesn't use the OS function to draw the
4668 				// text, we gotta treat it more as a file than as
4669 				// a drawing api.
4670 				osfont.getTtfBytes(),
4671 				18, // need to respecify size since opengl world is different coordinate system
4672 
4673 				// these last two numbers are why it is called
4674 				// "Limited" font. It only loads the characters
4675 				// in the given range, since the texture atlas
4676 				// it references is all a big image generated ahead
4677 				// of time. You could maybe do the whole thing but
4678 				// idk how much memory that is.
4679 				//
4680 				// But here, 0-128 represents the ASCII range, so
4681 				// good enough for most English things, numeric labels,
4682 				// etc.
4683 				0,
4684 				128
4685 			);
4686 		};
4687 
4688 		widget.redrawOpenGlScene = () {
4689 			// now we can use the glfont's drawString function
4690 
4691 			// first some opengl setup. You can do this in one place
4692 			// on window first visible too in many cases, just showing
4693 			// here cuz it is easier for me.
4694 
4695 			// gonna need some alpha blending or it just looks awful
4696 			glEnable(GL_BLEND);
4697 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
4698 			glClearColor(0,0,0,0);
4699 			glDepthFunc(GL_LEQUAL);
4700 
4701 			// Also need to enable 2d textures, since it draws the
4702 			// font characters as images baked in
4703 			glMatrixMode(GL_MODELVIEW);
4704 			glLoadIdentity();
4705 			glDisable(GL_DEPTH_TEST);
4706 			glEnable(GL_TEXTURE_2D);
4707 
4708 			// the orthographic matrix is best for 2d things like text
4709 			// so let's set that up. This matrix makes the coordinates
4710 			// in the opengl scene be one-to-one with the actual pixels
4711 			// on screen. (Not necessarily best, you may wish to scale
4712 			// things, but it does help keep fonts looking normal.)
4713 			glMatrixMode(GL_PROJECTION);
4714 			glLoadIdentity();
4715 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
4716 
4717 			// you can do other glScale, glRotate, glTranslate, etc
4718 			// to the matrix here of course if you want.
4719 
4720 			// note the x,y coordinates here are for the text baseline
4721 			// NOT the upper-left corner. The baseline is like the line
4722 			// in the notebook you write on. Most the letters are actually
4723 			// above it, but some, like p and q, dip a bit below it.
4724 			//
4725 			// So if you're used to the upper left coordinate like the
4726 			// rest of simpledisplay/minigui usually do, do the
4727 			// y + glfont.ascent to bring it down a little. So this
4728 			// example puts the string in the upper left of the window.
4729 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
4730 
4731 			// re color btw: the function sets a solid color internally,
4732 			// but you actually COULD do your own thing for rainbow effects
4733 			// and the sort if you wanted too, by pulling its guts out.
4734 			// Just view its source for an idea of how it actually draws:
4735 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
4736 
4737 			// it gets a bit complicated with the character positioning,
4738 			// but the opengl parts are fairly simple: bind a texture,
4739 			// set the color, draw a quad for each letter.
4740 
4741 
4742 			// the last optional argument there btw is a bounding box
4743 			// it will/ use to word wrap and return an object you can
4744 			// use to implement scrolling or pagination; it tells how
4745 			// much of the string didn't fit in the box. But for simple
4746 			// labels we can just ignore that.
4747 
4748 
4749 			// I'd suggest drawing text as the last step, after you
4750 			// do your other drawing. You might use the push/pop matrix
4751 			// stuff to keep your place. You, in theory, should be able
4752 			// to do text in a 3d space but I've never actually tried
4753 			// that....
4754 		};
4755 
4756 		window.loop();
4757 	}
4758 }
4759 
4760 version(custom_widgets)
4761 	private alias ListWidgetBase = ScrollableWidget;
4762 else
4763 	private alias ListWidgetBase = Widget;
4764 
4765 /++
4766 	A list widget contains a list of strings that the user can examine and select.
4767 
4768 
4769 	In the future, items in the list may be possible to be more than just strings.
4770 
4771 	See_Also:
4772 		[TableView]
4773 +/
4774 class ListWidget : ListWidgetBase {
4775 	/// Sends a change event when the selection changes, but the data is not attached to the event. You must instead loop the options to see if they are selected.
4776 	mixin Emits!(ChangeEvent!void);
4777 
4778 	static struct Option {
4779 		string label;
4780 		bool selected;
4781 		void* tag;
4782 	}
4783 
4784 	/++
4785 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
4786 	+/
4787 	void setSelection(int y) {
4788 		if(!multiSelect)
4789 			foreach(ref opt; options)
4790 				opt.selected = false;
4791 		if(y >= 0 && y < options.length)
4792 			options[y].selected = !options[y].selected;
4793 
4794 		this.emit!(ChangeEvent!void)(delegate {});
4795 
4796 		version(custom_widgets)
4797 			redraw();
4798 	}
4799 
4800 	/++
4801 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
4802 		Returns -1 if nothing is selected.
4803 	+/
4804 	int getSelection()
4805 	{
4806 		foreach(i, opt; options) {
4807 			if (opt.selected)
4808 				return cast(int) i;
4809 		}
4810 		return -1;
4811 	}
4812 
4813 	version(custom_widgets)
4814 	override void defaultEventHandler_click(ClickEvent event) {
4815 		this.focus();
4816 		if(event.button == MouseButton.left) {
4817 			auto y = (event.clientY - 4) / defaultLineHeight;
4818 			if(y >= 0 && y < options.length) {
4819 				setSelection(y);
4820 			}
4821 		}
4822 		super.defaultEventHandler_click(event);
4823 	}
4824 
4825 	this(Widget parent) {
4826 		tabStop = false;
4827 		super(parent);
4828 		version(win32_widgets)
4829 			createWin32Window(this, WC_LISTBOX, "",
4830 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
4831 	}
4832 
4833 	version(win32_widgets)
4834 	override void handleWmCommand(ushort code, ushort id) {
4835 		switch(code) {
4836 			case LBN_SELCHANGE:
4837 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
4838 				setSelection(cast(int) sel);
4839 			break;
4840 			default:
4841 		}
4842 	}
4843 
4844 
4845 	version(custom_widgets)
4846 	override void paintFrameAndBackground(WidgetPainter painter) {
4847 		draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor);
4848 	}
4849 
4850 	version(custom_widgets)
4851 	override void paint(WidgetPainter painter) {
4852 		auto cs = getComputedStyle();
4853 		auto pos = Point(4, 4);
4854 		foreach(idx, option; options) {
4855 			painter.fillColor = painter.visualTheme.widgetBackgroundColor;
4856 			painter.outlineColor = painter.visualTheme.widgetBackgroundColor;
4857 			painter.drawRectangle(pos, width - 8, defaultLineHeight);
4858 			if(option.selected) {
4859 				//painter.rasterOp = RasterOp.xor;
4860 				painter.outlineColor = cs.selectionForegroundColor;
4861 				painter.fillColor = cs.selectionBackgroundColor;
4862 				painter.drawRectangle(pos, width - 8, defaultLineHeight);
4863 				//painter.rasterOp = RasterOp.normal;
4864 			}
4865 			painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor;
4866 			painter.drawText(pos, option.label);
4867 			pos.y += defaultLineHeight;
4868 		}
4869 	}
4870 
4871 	static class Style : Widget.Style {
4872 		override WidgetBackground background() {
4873 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
4874 		}
4875 	}
4876 	mixin OverrideStyle!Style;
4877 	//mixin Padding!q{2};
4878 
4879 	void addOption(string text, void* tag = null) {
4880 		options ~= Option(text, false, tag);
4881 		version(win32_widgets) {
4882 			WCharzBuffer buffer = WCharzBuffer(text);
4883 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
4884 		}
4885 		version(custom_widgets) {
4886 			setContentSize(width, cast(int) (options.length * defaultLineHeight));
4887 			redraw();
4888 		}
4889 	}
4890 
4891 	void clear() {
4892 		options = null;
4893 		version(win32_widgets) {
4894 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
4895 				{}
4896 
4897 		} else version(custom_widgets) {
4898 			scrollTo(Point(0, 0));
4899 			redraw();
4900 		}
4901 	}
4902 
4903 	Option[] options;
4904 	version(win32_widgets)
4905 		enum multiSelect = false; /// not implemented yet
4906 	else
4907 		bool multiSelect;
4908 
4909 	override int heightStretchiness() { return 6; }
4910 }
4911 
4912 
4913 
4914 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
4915 enum ScrollBarShowPolicy {
4916 	automatic, /// automatically show the scroll bar if it is necessary
4917 	never, /// never show the scroll bar (scrolling must be done programmatically)
4918 	always /// always show the scroll bar, even if it is disabled
4919 }
4920 
4921 /++
4922 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
4923 
4924 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
4925 +/
4926 // FIXME ScrollBarShowPolicy
4927 // FIXME: use the ScrollMessageWidget in here now that it exists
4928 class ScrollableWidget : Widget {
4929 	// FIXME: make line size configurable
4930 	// FIXME: add keyboard controls
4931 	version(win32_widgets) {
4932 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
4933 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
4934 				auto pos = HIWORD(wParam);
4935 				auto m = LOWORD(wParam);
4936 
4937 				// FIXME: I can reintroduce the
4938 				// scroll bars now by using this
4939 				// in the top-level window handler
4940 				// to forward comamnds
4941 				auto scrollbarHwnd = lParam;
4942 				switch(m) {
4943 					case SB_BOTTOM:
4944 						if(msg == WM_HSCROLL)
4945 							horizontalScrollTo(contentWidth_);
4946 						else
4947 							verticalScrollTo(contentHeight_);
4948 					break;
4949 					case SB_TOP:
4950 						if(msg == WM_HSCROLL)
4951 							horizontalScrollTo(0);
4952 						else
4953 							verticalScrollTo(0);
4954 					break;
4955 					case SB_ENDSCROLL:
4956 						// idk
4957 					break;
4958 					case SB_LINEDOWN:
4959 						if(msg == WM_HSCROLL)
4960 							horizontalScroll(scaleWithDpi(16));
4961 						else
4962 							verticalScroll(scaleWithDpi(16));
4963 					break;
4964 					case SB_LINEUP:
4965 						if(msg == WM_HSCROLL)
4966 							horizontalScroll(scaleWithDpi(-16));
4967 						else
4968 							verticalScroll(scaleWithDpi(-16));
4969 					break;
4970 					case SB_PAGEDOWN:
4971 						if(msg == WM_HSCROLL)
4972 							horizontalScroll(scaleWithDpi(100));
4973 						else
4974 							verticalScroll(scaleWithDpi(100));
4975 					break;
4976 					case SB_PAGEUP:
4977 						if(msg == WM_HSCROLL)
4978 							horizontalScroll(scaleWithDpi(-100));
4979 						else
4980 							verticalScroll(scaleWithDpi(-100));
4981 					break;
4982 					case SB_THUMBPOSITION:
4983 					case SB_THUMBTRACK:
4984 						if(msg == WM_HSCROLL)
4985 							horizontalScrollTo(pos);
4986 						else
4987 							verticalScrollTo(pos);
4988 
4989 						if(m == SB_THUMBTRACK) {
4990 							// the event loop doesn't seem to carry on with a requested redraw..
4991 							// so we request it to get our dirty bit set...
4992 							redraw();
4993 
4994 							// then we need to immediately actually redraw it too for instant feedback to user
4995 
4996 							SimpleWindow.processAllCustomEvents();
4997 							//if(parentWindow)
4998 								//parentWindow.actualRedraw();
4999 						}
5000 					break;
5001 					default:
5002 				}
5003 			}
5004 			return super.hookedWndProc(msg, wParam, lParam);
5005 		}
5006 	}
5007 	///
5008 	this(Widget parent) {
5009 		this.parentWindow = parent.parentWindow;
5010 
5011 		version(win32_widgets) {
5012 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
5013 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
5014 			super(parent);
5015 		} else version(custom_widgets) {
5016 			outerContainer = new InternalScrollableContainerWidget(this, parent);
5017 			super(outerContainer);
5018 		} else static assert(0);
5019 	}
5020 
5021 	version(custom_widgets)
5022 		InternalScrollableContainerWidget outerContainer;
5023 
5024 	override void defaultEventHandler_click(ClickEvent event) {
5025 		if(event.button == MouseButton.wheelUp)
5026 			verticalScroll(scaleWithDpi(-16));
5027 		if(event.button == MouseButton.wheelDown)
5028 			verticalScroll(scaleWithDpi(16));
5029 		super.defaultEventHandler_click(event);
5030 	}
5031 
5032 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5033 		switch(event.key) {
5034 			case Key.Left:
5035 				horizontalScroll(scaleWithDpi(-16));
5036 			break;
5037 			case Key.Right:
5038 				horizontalScroll(scaleWithDpi(16));
5039 			break;
5040 			case Key.Up:
5041 				verticalScroll(scaleWithDpi(-16));
5042 			break;
5043 			case Key.Down:
5044 				verticalScroll(scaleWithDpi(16));
5045 			break;
5046 			case Key.Home:
5047 				verticalScrollTo(0);
5048 			break;
5049 			case Key.End:
5050 				verticalScrollTo(contentHeight);
5051 			break;
5052 			case Key.PageUp:
5053 				verticalScroll(scaleWithDpi(-160));
5054 			break;
5055 			case Key.PageDown:
5056 				verticalScroll(scaleWithDpi(160));
5057 			break;
5058 			default:
5059 		}
5060 		super.defaultEventHandler_keydown(event);
5061 	}
5062 
5063 
5064 	version(win32_widgets)
5065 	override void recomputeChildLayout() {
5066 		super.recomputeChildLayout();
5067 		SCROLLINFO info;
5068 		info.cbSize = info.sizeof;
5069 		info.nPage = viewportHeight;
5070 		info.fMask = SIF_PAGE | SIF_RANGE;
5071 		info.nMin = 0;
5072 		info.nMax = contentHeight_;
5073 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5074 
5075 		info.cbSize = info.sizeof;
5076 		info.nPage = viewportWidth;
5077 		info.fMask = SIF_PAGE | SIF_RANGE;
5078 		info.nMin = 0;
5079 		info.nMax = contentWidth_;
5080 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5081 	}
5082 
5083 	/*
5084 		Scrolling
5085 		------------
5086 
5087 		You are assigned a width and a height by the layout engine, which
5088 		is your viewport box. However, you may draw more than that by setting
5089 		a contentWidth and contentHeight.
5090 
5091 		If these can be contained by the viewport, no scrollbar is displayed.
5092 		If they cannot fit though, it will automatically show scroll as necessary.
5093 
5094 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5095 		is zero, no vertical scrolling is performed.
5096 
5097 		If scrolling is necessary, the lib will automatically work with the bars.
5098 		When you redraw, the origin and clipping info in the painter is set so if
5099 		you just draw everything, it will work, but you can be more efficient by checking
5100 		the viewportWidth, viewportHeight, and scrollOrigin members.
5101 	*/
5102 
5103 	///
5104 	final @property int viewportWidth() {
5105 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5106 	}
5107 	///
5108 	final @property int viewportHeight() {
5109 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5110 	}
5111 
5112 	// FIXME property
5113 	Point scrollOrigin_;
5114 
5115 	///
5116 	final const(Point) scrollOrigin() {
5117 		return scrollOrigin_;
5118 	}
5119 
5120 	// the user sets these two
5121 	private int contentWidth_ = 0;
5122 	private int contentHeight_ = 0;
5123 
5124 	///
5125 	int contentWidth() { return contentWidth_; }
5126 	///
5127 	int contentHeight() { return contentHeight_; }
5128 
5129 	///
5130 	void setContentSize(int width, int height) {
5131 		contentWidth_ = width;
5132 		contentHeight_ = height;
5133 
5134 		version(custom_widgets) {
5135 			if(showingVerticalScroll || showingHorizontalScroll) {
5136 				outerContainer.queueRecomputeChildLayout();
5137 			}
5138 
5139 			if(showingVerticalScroll())
5140 				outerContainer.verticalScrollBar.redraw();
5141 			if(showingHorizontalScroll())
5142 				outerContainer.horizontalScrollBar.redraw();
5143 		} else version(win32_widgets) {
5144 			queueRecomputeChildLayout();
5145 		} else static assert(0);
5146 	}
5147 
5148 	///
5149 	void verticalScroll(int delta) {
5150 		verticalScrollTo(scrollOrigin.y + delta);
5151 	}
5152 	///
5153 	void verticalScrollTo(int pos) {
5154 		scrollOrigin_.y = pos;
5155 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5156 			scrollOrigin_.y = contentHeight - viewportHeight;
5157 
5158 		if(scrollOrigin_.y < 0)
5159 			scrollOrigin_.y = 0;
5160 
5161 		version(win32_widgets) {
5162 			SCROLLINFO info;
5163 			info.cbSize = info.sizeof;
5164 			info.fMask = SIF_POS;
5165 			info.nPos = scrollOrigin_.y;
5166 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5167 		} else version(custom_widgets) {
5168 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5169 		} else static assert(0);
5170 
5171 		redraw();
5172 	}
5173 
5174 	///
5175 	void horizontalScroll(int delta) {
5176 		horizontalScrollTo(scrollOrigin.x + delta);
5177 	}
5178 	///
5179 	void horizontalScrollTo(int pos) {
5180 		scrollOrigin_.x = pos;
5181 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5182 			scrollOrigin_.x = contentWidth - viewportWidth;
5183 
5184 		if(scrollOrigin_.x < 0)
5185 			scrollOrigin_.x = 0;
5186 
5187 		version(win32_widgets) {
5188 			SCROLLINFO info;
5189 			info.cbSize = info.sizeof;
5190 			info.fMask = SIF_POS;
5191 			info.nPos = scrollOrigin_.x;
5192 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5193 		} else version(custom_widgets) {
5194 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5195 		} else static assert(0);
5196 
5197 		redraw();
5198 	}
5199 	///
5200 	void scrollTo(Point p) {
5201 		verticalScrollTo(p.y);
5202 		horizontalScrollTo(p.x);
5203 	}
5204 
5205 	///
5206 	void ensureVisibleInScroll(Point p) {
5207 		auto rect = viewportRectangle();
5208 		if(rect.contains(p))
5209 			return;
5210 		if(p.x < rect.left)
5211 			horizontalScroll(p.x - rect.left);
5212 		else if(p.x > rect.right)
5213 			horizontalScroll(p.x - rect.right);
5214 
5215 		if(p.y < rect.top)
5216 			verticalScroll(p.y - rect.top);
5217 		else if(p.y > rect.bottom)
5218 			verticalScroll(p.y - rect.bottom);
5219 	}
5220 
5221 	///
5222 	void ensureVisibleInScroll(Rectangle rect) {
5223 		ensureVisibleInScroll(rect.upperLeft);
5224 		ensureVisibleInScroll(rect.lowerRight);
5225 	}
5226 
5227 	///
5228 	Rectangle viewportRectangle() {
5229 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5230 	}
5231 
5232 	///
5233 	bool showingHorizontalScroll() {
5234 		return contentWidth > width;
5235 	}
5236 	///
5237 	bool showingVerticalScroll() {
5238 		return contentHeight > height;
5239 	}
5240 
5241 	/// This is called before the ordinary paint delegate,
5242 	/// giving you a chance to draw the window frame, etc,
5243 	/// before the scroll clip takes effect
5244 	void paintFrameAndBackground(WidgetPainter painter) {
5245 		version(win32_widgets) {
5246 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5247 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5248 			// since the pen is null, to fill the whole space, we need the +1 on both.
5249 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5250 			SelectObject(painter.impl.hdc, p);
5251 			SelectObject(painter.impl.hdc, b);
5252 		}
5253 
5254 	}
5255 
5256 	// make space for the scroll bar, and that's it.
5257 	final override int paddingRight() { return scaleWithDpi(16); }
5258 	final override int paddingBottom() { return scaleWithDpi(16); }
5259 
5260 	/*
5261 		END SCROLLING
5262 	*/
5263 
5264 	override WidgetPainter draw() {
5265 		int x = this.x, y = this.y;
5266 		auto parent = this.parent;
5267 		while(parent) {
5268 			x += parent.x;
5269 			y += parent.y;
5270 			parent = parent.parent;
5271 		}
5272 
5273 		//version(win32_widgets) {
5274 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5275 		//} else {
5276 			auto painter = parentWindow.win.draw(true);
5277 		//}
5278 		painter.originX = x;
5279 		painter.originY = y;
5280 
5281 		painter.originX = painter.originX - scrollOrigin.x;
5282 		painter.originY = painter.originY - scrollOrigin.y;
5283 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
5284 
5285 		return WidgetPainter(painter, this);
5286 	}
5287 
5288 	mixin ScrollableChildren;
5289 }
5290 
5291 // you need to have a Point scrollOrigin in the class somewhere
5292 // and a paintFrameAndBackground
5293 private mixin template ScrollableChildren() {
5294 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5295 		if(hidden)
5296 			return;
5297 
5298 		//version(win32_widgets)
5299 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5300 
5301 		painter.originX = lox + x;
5302 		painter.originY = loy + y;
5303 
5304 		bool actuallyPainted = false;
5305 
5306 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
5307 		if(clip == Rectangle.init)
5308 			return;
5309 
5310 		if(force || redrawRequested) {
5311 			//painter.setClipRectangle(scrollOrigin, width, height);
5312 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5313 			paintFrameAndBackground(painter);
5314 		}
5315 
5316 		/+
5317 		version(win32_widgets) {
5318 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
5319 		}
5320 		+/
5321 
5322 		painter.originX = painter.originX - scrollOrigin.x;
5323 		painter.originY = painter.originY - scrollOrigin.y;
5324 		if(force || redrawRequested) {
5325 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
5326 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
5327 
5328 			//erase(painter); // we paintFrameAndBackground above so no need
5329 			if(painter.visualTheme)
5330 				painter.visualTheme.doPaint(this, painter);
5331 			else
5332 				paint(painter);
5333 
5334 			if(invalidate) {
5335 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5336 				// children are contained inside this, so no need to do extra work
5337 				invalidate = false;
5338 			}
5339 
5340 
5341 			actuallyPainted = true;
5342 			redrawRequested = false;
5343 		}
5344 
5345 		foreach(child; children) {
5346 			if(cast(FixedPosition) child)
5347 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5348 			else
5349 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5350 		}
5351 	}
5352 }
5353 
5354 private class InternalScrollableContainerInsideWidget : ContainerWidget {
5355 	ScrollableContainerWidget scw;
5356 
5357 	this(ScrollableContainerWidget parent) {
5358 		scw = parent;
5359 		super(parent);
5360 	}
5361 
5362 	version(custom_widgets)
5363 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5364 		if(hidden)
5365 			return;
5366 
5367 		bool actuallyPainted = false;
5368 
5369 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
5370 
5371 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
5372 		if(clip == Rectangle.init)
5373 			return;
5374 
5375 		painter.originX = lox + x - scrollOrigin.x;
5376 		painter.originY = loy + y - scrollOrigin.y;
5377 		if(force || redrawRequested) {
5378 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5379 
5380 			erase(painter);
5381 			if(painter.visualTheme)
5382 				painter.visualTheme.doPaint(this, painter);
5383 			else
5384 				paint(painter);
5385 
5386 			if(invalidate) {
5387 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5388 				// children are contained inside this, so no need to do extra work
5389 				invalidate = false;
5390 			}
5391 
5392 			actuallyPainted = true;
5393 			redrawRequested = false;
5394 		}
5395 		foreach(child; children) {
5396 			if(cast(FixedPosition) child)
5397 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5398 			else
5399 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5400 		}
5401 	}
5402 
5403 	version(custom_widgets)
5404 	override protected void addScrollPosition(ref int x, ref int y) {
5405 		x += scw.scrollX_;
5406 		y += scw.scrollY_;
5407 	}
5408 }
5409 
5410 /++
5411 	A widget meant to contain other widgets that may need to scroll.
5412 
5413 	Currently buggy.
5414 
5415 	History:
5416 		Added July 1, 2021 (dub v10.2)
5417 
5418 		On January 3, 2022, I tried to use it in a few other cases
5419 		and found it only worked well in the original test case. Since
5420 		it still sucks, I think I'm going to rewrite it again.
5421 +/
5422 class ScrollableContainerWidget : ContainerWidget {
5423 	///
5424 	this(Widget parent) {
5425 		super(parent);
5426 
5427 		container = new InternalScrollableContainerInsideWidget(this);
5428 		hsb = new HorizontalScrollbar(this);
5429 		vsb = new VerticalScrollbar(this);
5430 
5431 		tabStop = false;
5432 		container.tabStop = false;
5433 		magic = true;
5434 
5435 
5436 		vsb.addEventListener("scrolltonextline", () {
5437 			scrollBy(0, scaleWithDpi(16));
5438 		});
5439 		vsb.addEventListener("scrolltopreviousline", () {
5440 			scrollBy(0,scaleWithDpi( -16));
5441 		});
5442 		vsb.addEventListener("scrolltonextpage", () {
5443 			scrollBy(0, container.height);
5444 		});
5445 		vsb.addEventListener("scrolltopreviouspage", () {
5446 			scrollBy(0, -container.height);
5447 		});
5448 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
5449 			scrollTo(scrollX_, spe.value);
5450 		});
5451 
5452 		this.addEventListener(delegate (scope ClickEvent e) {
5453 			if(e.button == MouseButton.wheelUp) {
5454 				if(!e.defaultPrevented)
5455 					scrollBy(0, scaleWithDpi(-16));
5456 				e.stopPropagation();
5457 			} else if(e.button == MouseButton.wheelDown) {
5458 				if(!e.defaultPrevented)
5459 					scrollBy(0, scaleWithDpi(16));
5460 				e.stopPropagation();
5461 			}
5462 		});
5463 	}
5464 
5465 	/+
5466 	override void defaultEventHandler_click(ClickEvent e) {
5467 	}
5468 	+/
5469 
5470 	override void removeAllChildren() {
5471 		container.removeAllChildren();
5472 	}
5473 
5474 	void scrollTo(int x, int y) {
5475 		scrollBy(x - scrollX_, y - scrollY_);
5476 	}
5477 
5478 	void scrollBy(int x, int y) {
5479 		auto ox = scrollX_;
5480 		auto oy = scrollY_;
5481 
5482 		auto nx = ox + x;
5483 		auto ny = oy + y;
5484 
5485 		if(nx < 0)
5486 			nx = 0;
5487 		if(ny < 0)
5488 			ny = 0;
5489 
5490 		auto maxX = hsb.max - container.width;
5491 		if(maxX < 0) maxX = 0;
5492 		auto maxY = vsb.max - container.height;
5493 		if(maxY < 0) maxY = 0;
5494 
5495 		if(nx > maxX)
5496 			nx = maxX;
5497 		if(ny > maxY)
5498 			ny = maxY;
5499 
5500 		auto dx = nx - ox;
5501 		auto dy = ny - oy;
5502 
5503 		if(dx || dy) {
5504 			version(win32_widgets)
5505 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
5506 			else {
5507 				redraw();
5508 			}
5509 
5510 			hsb.setPosition = nx;
5511 			vsb.setPosition = ny;
5512 
5513 			scrollX_ = nx;
5514 			scrollY_ = ny;
5515 		}
5516 	}
5517 
5518 	private int scrollX_;
5519 	private int scrollY_;
5520 
5521 	void setTotalArea(int width, int height) {
5522 		hsb.setMax(width);
5523 		vsb.setMax(height);
5524 	}
5525 
5526 	///
5527 	void setViewableArea(int width, int height) {
5528 		hsb.setViewableArea(width);
5529 		vsb.setViewableArea(height);
5530 	}
5531 
5532 	private bool magic;
5533 	override void addChild(Widget w, int position = int.max) {
5534 		if(magic)
5535 			container.addChild(w, position);
5536 		else
5537 			super.addChild(w, position);
5538 	}
5539 
5540 	override void recomputeChildLayout() {
5541 		if(hsb is null || vsb is null || container is null) return;
5542 
5543 		/+
5544 		writeln(x, " ", y , " ", width, " ", height);
5545 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
5546 		+/
5547 
5548 		registerMovement();
5549 
5550 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
5551 		hsb.x = 0;
5552 		hsb.y = this.height - hsb.height;
5553 		hsb.width = this.width - scaleWithDpi(16);
5554 		hsb.recomputeChildLayout();
5555 
5556 		vsb.width = scaleWithDpi(16); // FIXME?
5557 		vsb.x = this.width - vsb.width;
5558 		vsb.y = 0;
5559 		vsb.height = this.height - scaleWithDpi(16);
5560 		vsb.recomputeChildLayout();
5561 
5562 		container.x = 0;
5563 		container.y = 0;
5564 		container.width = this.width - vsb.width;
5565 		container.height = this.height - hsb.height;
5566 		container.recomputeChildLayout();
5567 
5568 		scrollX_ = 0;
5569 		scrollY_ = 0;
5570 
5571 		hsb.setPosition(0);
5572 		vsb.setPosition(0);
5573 
5574 		int mw, mh;
5575 		Widget c = container;
5576 		// FIXME: hack here to handle a layout inside...
5577 		if(c.children.length == 1 && cast(Layout) c.children[0])
5578 			c = c.children[0];
5579 		foreach(child; c.children) {
5580 			auto w = child.x + child.width;
5581 			auto h = child.y + child.height;
5582 
5583 			if(w > mw) mw = w;
5584 			if(h > mh) mh = h;
5585 		}
5586 
5587 		setTotalArea(mw, mh);
5588 		setViewableArea(width, height);
5589 	}
5590 
5591 	override int minHeight() { return scaleWithDpi(64); }
5592 
5593 	HorizontalScrollbar hsb;
5594 	VerticalScrollbar vsb;
5595 	ContainerWidget container;
5596 }
5597 
5598 
5599 version(custom_widgets)
5600 private class InternalScrollableContainerWidget : Widget {
5601 
5602 	ScrollableWidget sw;
5603 
5604 	VerticalScrollbar verticalScrollBar;
5605 	HorizontalScrollbar horizontalScrollBar;
5606 
5607 	this(ScrollableWidget sw, Widget parent) {
5608 		this.sw = sw;
5609 
5610 		this.tabStop = false;
5611 
5612 		super(parent);
5613 
5614 		horizontalScrollBar = new HorizontalScrollbar(this);
5615 		verticalScrollBar = new VerticalScrollbar(this);
5616 
5617 		horizontalScrollBar.showing_ = false;
5618 		verticalScrollBar.showing_ = false;
5619 
5620 		horizontalScrollBar.addEventListener("scrolltonextline", {
5621 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
5622 			sw.horizontalScrollTo(horizontalScrollBar.position);
5623 		});
5624 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
5625 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
5626 			sw.horizontalScrollTo(horizontalScrollBar.position);
5627 		});
5628 		verticalScrollBar.addEventListener("scrolltonextline", {
5629 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
5630 			sw.verticalScrollTo(verticalScrollBar.position);
5631 		});
5632 		verticalScrollBar.addEventListener("scrolltopreviousline", {
5633 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
5634 			sw.verticalScrollTo(verticalScrollBar.position);
5635 		});
5636 		horizontalScrollBar.addEventListener("scrolltonextpage", {
5637 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
5638 			sw.horizontalScrollTo(horizontalScrollBar.position);
5639 		});
5640 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
5641 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
5642 			sw.horizontalScrollTo(horizontalScrollBar.position);
5643 		});
5644 		verticalScrollBar.addEventListener("scrolltonextpage", {
5645 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
5646 			sw.verticalScrollTo(verticalScrollBar.position);
5647 		});
5648 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
5649 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
5650 			sw.verticalScrollTo(verticalScrollBar.position);
5651 		});
5652 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
5653 			horizontalScrollBar.setPosition(event.intValue);
5654 			sw.horizontalScrollTo(horizontalScrollBar.position);
5655 		});
5656 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
5657 			verticalScrollBar.setPosition(event.intValue);
5658 			sw.verticalScrollTo(verticalScrollBar.position);
5659 		});
5660 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
5661 			horizontalScrollBar.setPosition(event.intValue);
5662 			sw.horizontalScrollTo(horizontalScrollBar.position);
5663 		});
5664 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
5665 			verticalScrollBar.setPosition(event.intValue);
5666 		});
5667 	}
5668 
5669 	// this is supposed to be basically invisible...
5670 	override int minWidth() { return sw.minWidth; }
5671 	override int minHeight() { return sw.minHeight; }
5672 	override int maxWidth() { return sw.maxWidth; }
5673 	override int maxHeight() { return sw.maxHeight; }
5674 	override int widthStretchiness() { return sw.widthStretchiness; }
5675 	override int heightStretchiness() { return sw.heightStretchiness; }
5676 	override int marginLeft() { return sw.marginLeft; }
5677 	override int marginRight() { return sw.marginRight; }
5678 	override int marginTop() { return sw.marginTop; }
5679 	override int marginBottom() { return sw.marginBottom; }
5680 	override int paddingLeft() { return sw.paddingLeft; }
5681 	override int paddingRight() { return sw.paddingRight; }
5682 	override int paddingTop() { return sw.paddingTop; }
5683 	override int paddingBottom() { return sw.paddingBottom; }
5684 	override void focus() { sw.focus(); }
5685 
5686 
5687 	override void recomputeChildLayout() {
5688 		// The stupid thing needs to calculate if a scroll bar is needed...
5689 		recomputeChildLayoutHelper();
5690 		// then running it again will position things correctly if the bar is NOT needed
5691 		recomputeChildLayoutHelper();
5692 
5693 		// this sucks but meh it barely works
5694 	}
5695 
5696 	private void recomputeChildLayoutHelper() {
5697 		if(sw is null) return;
5698 
5699 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
5700 		if(horizontalScrollBar && verticalScrollBar) {
5701 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
5702 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
5703 			horizontalScrollBar.x = 0;
5704 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
5705 
5706 			verticalScrollBar.width = verticalScrollBar.minWidth();
5707 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
5708 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
5709 			verticalScrollBar.y = 0 + 2;
5710 
5711 			sw.x = 0;
5712 			sw.y = 0;
5713 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
5714 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
5715 
5716 			if(sw.contentWidth_ <= this.width)
5717 				sw.scrollOrigin_.x = 0;
5718 			if(sw.contentHeight_ <= this.height)
5719 				sw.scrollOrigin_.y = 0;
5720 
5721 			horizontalScrollBar.recomputeChildLayout();
5722 			verticalScrollBar.recomputeChildLayout();
5723 			sw.recomputeChildLayout();
5724 		}
5725 
5726 		if(sw.contentWidth_ <= this.width)
5727 			sw.scrollOrigin_.x = 0;
5728 		if(sw.contentHeight_ <= this.height)
5729 			sw.scrollOrigin_.y = 0;
5730 
5731 		if(sw.showingHorizontalScroll())
5732 			horizontalScrollBar.showing(true, false);
5733 		else
5734 			horizontalScrollBar.showing(false, false);
5735 		if(sw.showingVerticalScroll())
5736 			verticalScrollBar.showing(true, false);
5737 		else
5738 			verticalScrollBar.showing(false, false);
5739 
5740 		verticalScrollBar.setViewableArea(sw.viewportHeight());
5741 		verticalScrollBar.setMax(sw.contentHeight);
5742 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
5743 
5744 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
5745 		horizontalScrollBar.setMax(sw.contentWidth);
5746 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
5747 	}
5748 }
5749 
5750 /*
5751 class ScrollableClientWidget : Widget {
5752 	this(Widget parent) {
5753 		super(parent);
5754 	}
5755 	override void paint(WidgetPainter p) {
5756 		parent.paint(p);
5757 	}
5758 }
5759 */
5760 
5761 /++
5762 	A slider, also known as a trackbar control, is commonly used in applications like volume controls where you want the user to select a value between a min and a max without needing a specific value or otherwise precise input.
5763 +/
5764 abstract class Slider : Widget {
5765 	this(int min, int max, int step, Widget parent) {
5766 		min_ = min;
5767 		max_ = max;
5768 		step_ = step;
5769 		page_ = step;
5770 		super(parent);
5771 	}
5772 
5773 	private int min_;
5774 	private int max_;
5775 	private int step_;
5776 	private int position_;
5777 	private int page_;
5778 
5779 	// selection start and selection end
5780 	// tics
5781 	// tooltip?
5782 	// some way to see and just type the value
5783 	// win32 buddy controls are labels
5784 
5785 	///
5786 	void setMin(int a) {
5787 		min_ = a;
5788 		version(custom_widgets)
5789 			redraw();
5790 		version(win32_widgets)
5791 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
5792 	}
5793 	///
5794 	int min() {
5795 		return min_;
5796 	}
5797 	///
5798 	void setMax(int a) {
5799 		max_ = a;
5800 		version(custom_widgets)
5801 			redraw();
5802 		version(win32_widgets)
5803 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
5804 	}
5805 	///
5806 	int max() {
5807 		return max_;
5808 	}
5809 	///
5810 	void setPosition(int a) {
5811 		if(a > max)
5812 			a = max;
5813 		if(a < min)
5814 			a = min;
5815 		position_ = a;
5816 		version(custom_widgets)
5817 			setPositionCustom(a);
5818 
5819 		version(win32_widgets)
5820 			setPositionWindows(a);
5821 	}
5822 	version(win32_widgets) {
5823 		protected abstract void setPositionWindows(int a);
5824 	}
5825 
5826 	protected abstract int win32direction();
5827 
5828 	/++
5829 		Alias for [position] for better compatibility with generic code.
5830 
5831 		History:
5832 			Added October 5, 2021
5833 	+/
5834 	@property int value() {
5835 		return position;
5836 	}
5837 
5838 	///
5839 	int position() {
5840 		return position_;
5841 	}
5842 	///
5843 	void setStep(int a) {
5844 		step_ = a;
5845 		version(win32_widgets)
5846 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
5847 	}
5848 	///
5849 	int step() {
5850 		return step_;
5851 	}
5852 	///
5853 	void setPageSize(int a) {
5854 		page_ = a;
5855 		version(win32_widgets)
5856 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
5857 	}
5858 	///
5859 	int pageSize() {
5860 		return page_;
5861 	}
5862 
5863 	private void notify() {
5864 		auto event = new ChangeEvent!int(this, &this.position);
5865 		event.dispatch();
5866 	}
5867 
5868 	version(win32_widgets)
5869 	void win32Setup(int style) {
5870 		createWin32Window(this, TRACKBAR_CLASS, "",
5871 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
5872 
5873 		// the trackbar sends the same messages as scroll, which
5874 		// our other layer sends as these... just gonna translate
5875 		// here
5876 		this.addDirectEventListener("scrolltoposition", (Event event) {
5877 			event.stopPropagation();
5878 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
5879 			notify();
5880 		});
5881 		this.addDirectEventListener("scrolltonextline", (Event event) {
5882 			event.stopPropagation();
5883 			this.setPosition(this.position + this.step_ * this.win32direction);
5884 			notify();
5885 		});
5886 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
5887 			event.stopPropagation();
5888 			this.setPosition(this.position - this.step_ * this.win32direction);
5889 			notify();
5890 		});
5891 		this.addDirectEventListener("scrolltonextpage", (Event event) {
5892 			event.stopPropagation();
5893 			this.setPosition(this.position + this.page_ * this.win32direction);
5894 			notify();
5895 		});
5896 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
5897 			event.stopPropagation();
5898 			this.setPosition(this.position - this.page_ * this.win32direction);
5899 			notify();
5900 		});
5901 
5902 		setMin(min_);
5903 		setMax(max_);
5904 		setStep(step_);
5905 		setPageSize(page_);
5906 	}
5907 
5908 	version(custom_widgets) {
5909 		protected MouseTrackingWidget thumb;
5910 
5911 		protected abstract void setPositionCustom(int a);
5912 
5913 		override void defaultEventHandler_keydown(KeyDownEvent event) {
5914 			switch(event.key) {
5915 				case Key.Up:
5916 				case Key.Right:
5917 					setPosition(position() - step() * win32direction);
5918 					changed();
5919 				break;
5920 				case Key.Down:
5921 				case Key.Left:
5922 					setPosition(position() + step() * win32direction);
5923 					changed();
5924 				break;
5925 				case Key.Home:
5926 					setPosition(win32direction > 0 ? min() : max());
5927 					changed();
5928 				break;
5929 				case Key.End:
5930 					setPosition(win32direction > 0 ? max() : min());
5931 					changed();
5932 				break;
5933 				case Key.PageUp:
5934 					setPosition(position() - pageSize() * win32direction);
5935 					changed();
5936 				break;
5937 				case Key.PageDown:
5938 					setPosition(position() + pageSize() * win32direction);
5939 					changed();
5940 				break;
5941 				default:
5942 			}
5943 			super.defaultEventHandler_keydown(event);
5944 		}
5945 
5946 		protected void changed() {
5947 			auto ev = new ChangeEvent!int(this, &position);
5948 			ev.dispatch();
5949 		}
5950 	}
5951 }
5952 
5953 /++
5954 
5955 +/
5956 class VerticalSlider : Slider {
5957 	this(int min, int max, int step, Widget parent) {
5958 		version(custom_widgets)
5959 			initialize();
5960 
5961 		super(min, max, step, parent);
5962 
5963 		version(win32_widgets)
5964 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
5965 	}
5966 
5967 	protected override int win32direction() {
5968 		return -1;
5969 	}
5970 
5971 	version(win32_widgets)
5972 	protected override void setPositionWindows(int a) {
5973 		// the windows thing makes the top 0 and i don't like that.
5974 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
5975 	}
5976 
5977 	version(custom_widgets)
5978 	private void initialize() {
5979 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
5980 
5981 		thumb.tabStop = false;
5982 
5983 		thumb.thumbWidth = width;
5984 		thumb.thumbHeight = scaleWithDpi(16);
5985 
5986 		thumb.addEventListener(EventType.change, () {
5987 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
5988 			sx = max - sx;
5989 			//informProgramThatUserChangedPosition(sx);
5990 
5991 			position_ = sx;
5992 
5993 			changed();
5994 		});
5995 	}
5996 
5997 	version(custom_widgets)
5998 	override void recomputeChildLayout() {
5999 		thumb.thumbWidth = this.width;
6000 		super.recomputeChildLayout();
6001 		setPositionCustom(position_);
6002 	}
6003 
6004 	version(custom_widgets)
6005 	protected override void setPositionCustom(int a) {
6006 		if(max())
6007 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
6008 		redraw();
6009 	}
6010 }
6011 
6012 /++
6013 
6014 +/
6015 class HorizontalSlider : Slider {
6016 	this(int min, int max, int step, Widget parent) {
6017 		version(custom_widgets)
6018 			initialize();
6019 
6020 		super(min, max, step, parent);
6021 
6022 		version(win32_widgets)
6023 			win32Setup(TBS_HORZ);
6024 	}
6025 
6026 	version(win32_widgets)
6027 	protected override void setPositionWindows(int a) {
6028 		SendMessage(hwnd, TBM_SETPOS, true, a);
6029 	}
6030 
6031 	protected override int win32direction() {
6032 		return 1;
6033 	}
6034 
6035 	version(custom_widgets)
6036 	private void initialize() {
6037 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6038 
6039 		thumb.tabStop = false;
6040 
6041 		thumb.thumbWidth = scaleWithDpi(16);
6042 		thumb.thumbHeight = height;
6043 
6044 		thumb.addEventListener(EventType.change, () {
6045 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6046 			//informProgramThatUserChangedPosition(sx);
6047 
6048 			position_ = sx;
6049 
6050 			changed();
6051 		});
6052 	}
6053 
6054 	version(custom_widgets)
6055 	override void recomputeChildLayout() {
6056 		thumb.thumbHeight = this.height;
6057 		super.recomputeChildLayout();
6058 		setPositionCustom(position_);
6059 	}
6060 
6061 	version(custom_widgets)
6062 	protected override void setPositionCustom(int a) {
6063 		if(max())
6064 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6065 		redraw();
6066 	}
6067 }
6068 
6069 
6070 ///
6071 abstract class ScrollbarBase : Widget {
6072 	///
6073 	this(Widget parent) {
6074 		super(parent);
6075 		tabStop = false;
6076 		step_ = scaleWithDpi(16);
6077 	}
6078 
6079 	private int viewableArea_;
6080 	private int max_;
6081 	private int step_;// = 16;
6082 	private int position_;
6083 
6084 	///
6085 	bool atEnd() {
6086 		return position_ + viewableArea_ >= max_;
6087 	}
6088 
6089 	///
6090 	bool atStart() {
6091 		return position_ == 0;
6092 	}
6093 
6094 	///
6095 	void setViewableArea(int a) {
6096 		viewableArea_ = a;
6097 		version(custom_widgets)
6098 			redraw();
6099 	}
6100 	///
6101 	void setMax(int a) {
6102 		max_ = a;
6103 		version(custom_widgets)
6104 			redraw();
6105 	}
6106 	///
6107 	int max() {
6108 		return max_;
6109 	}
6110 	///
6111 	void setPosition(int a) {
6112 		auto logicalMax = max_ - viewableArea_;
6113 		if(a == int.max)
6114 			a = logicalMax;
6115 
6116 		if(a > logicalMax)
6117 			a = logicalMax;
6118 		if(a < 0)
6119 			a = 0;
6120 
6121 		position_ = a;
6122 
6123 		version(custom_widgets)
6124 			redraw();
6125 	}
6126 	///
6127 	int position() {
6128 		return position_;
6129 	}
6130 	///
6131 	void setStep(int a) {
6132 		step_ = a;
6133 	}
6134 	///
6135 	int step() {
6136 		return step_;
6137 	}
6138 
6139 	// FIXME: remove this.... maybe
6140 	/+
6141 	protected void informProgramThatUserChangedPosition(int n) {
6142 		position_ = n;
6143 		auto evt = new Event(EventType.change, this);
6144 		evt.intValue = n;
6145 		evt.dispatch();
6146 	}
6147 	+/
6148 
6149 	version(custom_widgets) {
6150 		enum MIN_THUMB_SIZE = 8;
6151 
6152 		abstract protected int getBarDim();
6153 		int thumbSize() {
6154 			if(viewableArea_ >= max_ || max_ == 0)
6155 				return getBarDim();
6156 
6157 			int res = viewableArea_ * getBarDim() / max_;
6158 
6159 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6160 				res = scaleWithDpi(MIN_THUMB_SIZE);
6161 
6162 			return res;
6163 		}
6164 
6165 		int thumbPosition() {
6166 			/*
6167 				viewableArea_ is the viewport height/width
6168 				position_ is where we are
6169 			*/
6170 			//if(position_ + viewableArea_ >= max_)
6171 				//return getBarDim - thumbSize;
6172 
6173 			auto maximumPossibleValue = getBarDim() - thumbSize;
6174 			auto maximiumLogicalValue = max_ - viewableArea_;
6175 
6176 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6177 
6178 			return p;
6179 		}
6180 	}
6181 }
6182 
6183 //public import mgt;
6184 
6185 /++
6186 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6187 
6188 	Concrete subclasses may include a scrollbar thumb and a volume control.
6189 +/
6190 //version(custom_widgets)
6191 class MouseTrackingWidget : Widget {
6192 
6193 	///
6194 	int positionX() { return positionX_; }
6195 	///
6196 	int positionY() { return positionY_; }
6197 
6198 	///
6199 	void positionX(int p) { positionX_ = p; }
6200 	///
6201 	void positionY(int p) { positionY_ = p; }
6202 
6203 	private int positionX_;
6204 	private int positionY_;
6205 
6206 	///
6207 	enum Orientation {
6208 		horizontal, ///
6209 		vertical, ///
6210 		twoDimensional, ///
6211 	}
6212 
6213 	private int thumbWidth_;
6214 	private int thumbHeight_;
6215 
6216 	///
6217 	int thumbWidth() { return thumbWidth_; }
6218 	///
6219 	int thumbHeight() { return thumbHeight_; }
6220 	///
6221 	int thumbWidth(int a) { return thumbWidth_ = a; }
6222 	///
6223 	int thumbHeight(int a) { return thumbHeight_ = a; }
6224 
6225 	private bool dragging;
6226 	private bool hovering;
6227 	private int startMouseX, startMouseY;
6228 
6229 	///
6230 	this(Orientation orientation, Widget parent) {
6231 		super(parent);
6232 
6233 		//assert(parentWindow !is null);
6234 
6235 		addEventListener((MouseDownEvent event) {
6236 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6237 				dragging = true;
6238 				startMouseX = event.clientX - positionX;
6239 				startMouseY = event.clientY - positionY;
6240 				parentWindow.captureMouse(this);
6241 			} else {
6242 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6243 					positionX = event.clientX - thumbWidth / 2;
6244 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6245 					positionY = event.clientY - thumbHeight / 2;
6246 
6247 				if(positionX + thumbWidth > this.width)
6248 					positionX = this.width - thumbWidth;
6249 				if(positionY + thumbHeight > this.height)
6250 					positionY = this.height - thumbHeight;
6251 
6252 				if(positionX < 0)
6253 					positionX = 0;
6254 				if(positionY < 0)
6255 					positionY = 0;
6256 
6257 
6258 				// this.emit!(ChangeEvent!void)();
6259 				auto evt = new Event(EventType.change, this);
6260 				evt.sendDirectly();
6261 
6262 				redraw();
6263 
6264 			}
6265 		});
6266 
6267 		addEventListener(EventType.mouseup, (Event event) {
6268 			dragging = false;
6269 			parentWindow.releaseMouseCapture();
6270 		});
6271 
6272 		addEventListener(EventType.mouseout, (Event event) {
6273 			if(!hovering)
6274 				return;
6275 			hovering = false;
6276 			redraw();
6277 		});
6278 
6279 		int lpx, lpy;
6280 
6281 		addEventListener((MouseMoveEvent event) {
6282 			auto oh = hovering;
6283 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6284 				hovering = true;
6285 			} else {
6286 				hovering = false;
6287 			}
6288 			if(!dragging) {
6289 				if(hovering != oh)
6290 					redraw();
6291 				return;
6292 			}
6293 
6294 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6295 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
6296 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6297 				positionY = event.clientY - startMouseY;
6298 
6299 			if(positionX + thumbWidth > this.width)
6300 				positionX = this.width - thumbWidth;
6301 			if(positionY + thumbHeight > this.height)
6302 				positionY = this.height - thumbHeight;
6303 
6304 			if(positionX < 0)
6305 				positionX = 0;
6306 			if(positionY < 0)
6307 				positionY = 0;
6308 
6309 			if(positionX != lpx || positionY != lpy) {
6310 				lpx = positionX;
6311 				lpy = positionY;
6312 
6313 				auto evt = new Event(EventType.change, this);
6314 				evt.sendDirectly();
6315 			}
6316 
6317 			redraw();
6318 		});
6319 	}
6320 
6321 	version(custom_widgets)
6322 	override void paint(WidgetPainter painter) {
6323 		auto cs = getComputedStyle();
6324 		auto c = darken(cs.windowBackgroundColor, 0.2);
6325 		painter.outlineColor = c;
6326 		painter.fillColor = c;
6327 		painter.drawRectangle(Point(0, 0), this.width, this.height);
6328 
6329 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
6330 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
6331 	}
6332 }
6333 
6334 //version(custom_widgets)
6335 //private
6336 class HorizontalScrollbar : ScrollbarBase {
6337 
6338 	version(custom_widgets) {
6339 		private MouseTrackingWidget thumb;
6340 
6341 		override int getBarDim() {
6342 			return thumb.width;
6343 		}
6344 	}
6345 
6346 	override void setViewableArea(int a) {
6347 		super.setViewableArea(a);
6348 
6349 		version(win32_widgets) {
6350 			SCROLLINFO info;
6351 			info.cbSize = info.sizeof;
6352 			info.nPage = a + 1;
6353 			info.fMask = SIF_PAGE;
6354 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6355 		} else version(custom_widgets) {
6356 			thumb.positionX = thumbPosition;
6357 			thumb.thumbWidth = thumbSize;
6358 			thumb.redraw();
6359 		} else static assert(0);
6360 
6361 	}
6362 
6363 	override void setMax(int a) {
6364 		super.setMax(a);
6365 		version(win32_widgets) {
6366 			SCROLLINFO info;
6367 			info.cbSize = info.sizeof;
6368 			info.nMin = 0;
6369 			info.nMax = max;
6370 			info.fMask = SIF_RANGE;
6371 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6372 		} else version(custom_widgets) {
6373 			thumb.positionX = thumbPosition;
6374 			thumb.thumbWidth = thumbSize;
6375 			thumb.redraw();
6376 		}
6377 	}
6378 
6379 	override void setPosition(int a) {
6380 		super.setPosition(a);
6381 		version(win32_widgets) {
6382 			SCROLLINFO info;
6383 			info.cbSize = info.sizeof;
6384 			info.fMask = SIF_POS;
6385 			info.nPos = position;
6386 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6387 		} else version(custom_widgets) {
6388 			thumb.positionX = thumbPosition();
6389 			thumb.thumbWidth = thumbSize;
6390 			thumb.redraw();
6391 		} else static assert(0);
6392 	}
6393 
6394 	this(Widget parent) {
6395 		super(parent);
6396 
6397 		version(win32_widgets) {
6398 			createWin32Window(this, "Scrollbar"w, "",
6399 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
6400 		} else version(custom_widgets) {
6401 			auto vl = new HorizontalLayout(this);
6402 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
6403 			leftButton.setClickRepeat(scrollClickRepeatInterval);
6404 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
6405 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
6406 			rightButton.setClickRepeat(scrollClickRepeatInterval);
6407 
6408 			leftButton.tabStop = false;
6409 			rightButton.tabStop = false;
6410 			thumb.tabStop = false;
6411 
6412 			leftButton.addEventListener(EventType.triggered, () {
6413 				this.emitCommand!"scrolltopreviousline"();
6414 				//informProgramThatUserChangedPosition(position - step());
6415 			});
6416 			rightButton.addEventListener(EventType.triggered, () {
6417 				this.emitCommand!"scrolltonextline"();
6418 				//informProgramThatUserChangedPosition(position + step());
6419 			});
6420 
6421 			thumb.thumbWidth = this.minWidth;
6422 			thumb.thumbHeight = scaleWithDpi(16);
6423 
6424 			thumb.addEventListener(EventType.change, () {
6425 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
6426 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
6427 
6428 				//informProgramThatUserChangedPosition(sx);
6429 
6430 				auto ev = new ScrollToPositionEvent(this, sx);
6431 				ev.dispatch();
6432 			});
6433 		}
6434 	}
6435 
6436 	override int minHeight() { return scaleWithDpi(16); }
6437 	override int maxHeight() { return scaleWithDpi(16); }
6438 	override int minWidth() { return scaleWithDpi(48); }
6439 }
6440 
6441 class ScrollToPositionEvent : Event {
6442 	enum EventString = "scrolltoposition";
6443 
6444 	this(Widget target, int value) {
6445 		this.value = value;
6446 		super(EventString, target);
6447 	}
6448 
6449 	immutable int value;
6450 
6451 	override @property int intValue() {
6452 		return value;
6453 	}
6454 }
6455 
6456 //version(custom_widgets)
6457 //private
6458 class VerticalScrollbar : ScrollbarBase {
6459 
6460 	version(custom_widgets) {
6461 		override int getBarDim() {
6462 			return thumb.height;
6463 		}
6464 
6465 		private MouseTrackingWidget thumb;
6466 	}
6467 
6468 	override void setViewableArea(int a) {
6469 		super.setViewableArea(a);
6470 
6471 		version(win32_widgets) {
6472 			SCROLLINFO info;
6473 			info.cbSize = info.sizeof;
6474 			info.nPage = a + 1;
6475 			info.fMask = SIF_PAGE;
6476 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6477 		} else version(custom_widgets) {
6478 			thumb.positionY = thumbPosition;
6479 			thumb.thumbHeight = thumbSize;
6480 			thumb.redraw();
6481 		} else static assert(0);
6482 
6483 	}
6484 
6485 	override void setMax(int a) {
6486 		super.setMax(a);
6487 		version(win32_widgets) {
6488 			SCROLLINFO info;
6489 			info.cbSize = info.sizeof;
6490 			info.nMin = 0;
6491 			info.nMax = max;
6492 			info.fMask = SIF_RANGE;
6493 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6494 		} else version(custom_widgets) {
6495 			thumb.positionY = thumbPosition;
6496 			thumb.thumbHeight = thumbSize;
6497 			thumb.redraw();
6498 		}
6499 	}
6500 
6501 	override void setPosition(int a) {
6502 		super.setPosition(a);
6503 		version(win32_widgets) {
6504 			SCROLLINFO info;
6505 			info.cbSize = info.sizeof;
6506 			info.fMask = SIF_POS;
6507 			info.nPos = position;
6508 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6509 		} else version(custom_widgets) {
6510 			thumb.positionY = thumbPosition;
6511 			thumb.thumbHeight = thumbSize;
6512 			thumb.redraw();
6513 		} else static assert(0);
6514 	}
6515 
6516 	this(Widget parent) {
6517 		super(parent);
6518 
6519 		version(win32_widgets) {
6520 			createWin32Window(this, "Scrollbar"w, "",
6521 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
6522 		} else version(custom_widgets) {
6523 			auto vl = new VerticalLayout(this);
6524 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
6525 			upButton.setClickRepeat(scrollClickRepeatInterval);
6526 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
6527 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
6528 			downButton.setClickRepeat(scrollClickRepeatInterval);
6529 
6530 			upButton.addEventListener(EventType.triggered, () {
6531 				this.emitCommand!"scrolltopreviousline"();
6532 				//informProgramThatUserChangedPosition(position - step());
6533 			});
6534 			downButton.addEventListener(EventType.triggered, () {
6535 				this.emitCommand!"scrolltonextline"();
6536 				//informProgramThatUserChangedPosition(position + step());
6537 			});
6538 
6539 			thumb.thumbWidth = this.minWidth;
6540 			thumb.thumbHeight = scaleWithDpi(16);
6541 
6542 			thumb.addEventListener(EventType.change, () {
6543 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
6544 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
6545 
6546 				auto ev = new ScrollToPositionEvent(this, sy);
6547 				ev.dispatch();
6548 
6549 				//informProgramThatUserChangedPosition(sy);
6550 			});
6551 
6552 			upButton.tabStop = false;
6553 			downButton.tabStop = false;
6554 			thumb.tabStop = false;
6555 		}
6556 	}
6557 
6558 	override int minWidth() { return scaleWithDpi(16); }
6559 	override int maxWidth() { return scaleWithDpi(16); }
6560 	override int minHeight() { return scaleWithDpi(48); }
6561 }
6562 
6563 
6564 /++
6565 	EXPERIMENTAL
6566 
6567 	A widget specialized for being a container for other widgets.
6568 
6569 	History:
6570 		Added May 29, 2021. Not stabilized at this time.
6571 +/
6572 class WidgetContainer : Widget {
6573 	this(Widget parent) {
6574 		tabStop = false;
6575 		super(parent);
6576 	}
6577 
6578 	override int maxHeight() {
6579 		if(this.children.length == 1) {
6580 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
6581 		} else {
6582 			return int.max;
6583 		}
6584 	}
6585 
6586 	override int maxWidth() {
6587 		if(this.children.length == 1) {
6588 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
6589 		} else {
6590 			return int.max;
6591 		}
6592 	}
6593 
6594 	/+
6595 
6596 	override int minHeight() {
6597 		int largest = 0;
6598 		int margins = 0;
6599 		int lastMargin = 0;
6600 		foreach(child; children) {
6601 			auto mh = child.minHeight();
6602 			if(mh > largest)
6603 				largest = mh;
6604 			margins += mymax(lastMargin, child.marginTop());
6605 			lastMargin = child.marginBottom();
6606 		}
6607 		return largest + margins;
6608 	}
6609 
6610 	override int maxHeight() {
6611 		int largest = 0;
6612 		int margins = 0;
6613 		int lastMargin = 0;
6614 		foreach(child; children) {
6615 			auto mh = child.maxHeight();
6616 			if(mh == int.max)
6617 				return int.max;
6618 			if(mh > largest)
6619 				largest = mh;
6620 			margins += mymax(lastMargin, child.marginTop());
6621 			lastMargin = child.marginBottom();
6622 		}
6623 		return largest + margins;
6624 	}
6625 
6626 	override int minWidth() {
6627 		int min;
6628 		foreach(child; children) {
6629 			auto cm = child.minWidth;
6630 			if(cm > min)
6631 				min = cm;
6632 		}
6633 		return min + paddingLeft + paddingRight;
6634 	}
6635 
6636 	override int minHeight() {
6637 		int min;
6638 		foreach(child; children) {
6639 			auto cm = child.minHeight;
6640 			if(cm > min)
6641 				min = cm;
6642 		}
6643 		return min + paddingTop + paddingBottom;
6644 	}
6645 
6646 	override int maxHeight() {
6647 		int largest = 0;
6648 		int margins = 0;
6649 		int lastMargin = 0;
6650 		foreach(child; children) {
6651 			auto mh = child.maxHeight();
6652 			if(mh == int.max)
6653 				return int.max;
6654 			if(mh > largest)
6655 				largest = mh;
6656 			margins += mymax(lastMargin, child.marginTop());
6657 			lastMargin = child.marginBottom();
6658 		}
6659 		return largest + margins;
6660 	}
6661 
6662 	override int heightStretchiness() {
6663 		int max;
6664 		foreach(child; children) {
6665 			auto c = child.heightStretchiness;
6666 			if(c > max)
6667 				max = c;
6668 		}
6669 		return max;
6670 	}
6671 
6672 	override int marginTop() {
6673 		if(this.children.length)
6674 			return this.children[0].marginTop;
6675 		return 0;
6676 	}
6677 	+/
6678 }
6679 
6680 ///
6681 abstract class Layout : Widget {
6682 	this(Widget parent) {
6683 		tabStop = false;
6684 		super(parent);
6685 	}
6686 }
6687 
6688 /++
6689 	Makes all children minimum width and height, placing them down
6690 	left to right, top to bottom.
6691 
6692 	Useful if you want to make a list of buttons that automatically
6693 	wrap to a new line when necessary.
6694 +/
6695 class InlineBlockLayout : Layout {
6696 	///
6697 	this(Widget parent) { super(parent); }
6698 
6699 	override void recomputeChildLayout() {
6700 		registerMovement();
6701 
6702 		int x = this.paddingLeft, y = this.paddingTop;
6703 
6704 		int lineHeight;
6705 		int previousMargin = 0;
6706 		int previousMarginBottom = 0;
6707 
6708 		foreach(child; children) {
6709 			if(child.hidden)
6710 				continue;
6711 			if(cast(FixedPosition) child) {
6712 				child.recomputeChildLayout();
6713 				continue;
6714 			}
6715 			child.width = child.flexBasisWidth();
6716 			if(child.width == 0)
6717 				child.width = child.minWidth();
6718 			if(child.width == 0)
6719 				child.width = 32;
6720 
6721 			child.height = child.flexBasisHeight();
6722 			if(child.height == 0)
6723 				child.height = child.minHeight();
6724 			if(child.height == 0)
6725 				child.height = 32;
6726 
6727 			if(x + child.width + paddingRight > this.width) {
6728 				x = this.paddingLeft;
6729 				y += lineHeight;
6730 				lineHeight = 0;
6731 				previousMargin = 0;
6732 				previousMarginBottom = 0;
6733 			}
6734 
6735 			auto margin = child.marginLeft;
6736 			if(previousMargin > margin)
6737 				margin = previousMargin;
6738 
6739 			x += margin;
6740 
6741 			child.x = x;
6742 			child.y = y;
6743 
6744 			int marginTopApplied;
6745 			if(child.marginTop > previousMarginBottom) {
6746 				child.y += child.marginTop;
6747 				marginTopApplied = child.marginTop;
6748 			}
6749 
6750 			x += child.width;
6751 			previousMargin = child.marginRight;
6752 
6753 			if(child.marginBottom > previousMarginBottom)
6754 				previousMarginBottom = child.marginBottom;
6755 
6756 			auto h = child.height + previousMarginBottom + marginTopApplied;
6757 			if(h > lineHeight)
6758 				lineHeight = h;
6759 
6760 			child.recomputeChildLayout();
6761 		}
6762 
6763 	}
6764 
6765 	override int minWidth() {
6766 		int min;
6767 		foreach(child; children) {
6768 			auto cm = child.minWidth;
6769 			if(cm > min)
6770 				min = cm;
6771 		}
6772 		return min + paddingLeft + paddingRight;
6773 	}
6774 
6775 	override int minHeight() {
6776 		int min;
6777 		foreach(child; children) {
6778 			auto cm = child.minHeight;
6779 			if(cm > min)
6780 				min = cm;
6781 		}
6782 		return min + paddingTop + paddingBottom;
6783 	}
6784 }
6785 
6786 /++
6787 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
6788 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
6789 	the [TabWidget] will automatically change pages of child widgets.
6790 
6791 	This allows you to react to it however you see fit rather than having to
6792 	be tied to just the new sets of child widgets.
6793 
6794 	It sends the message in the form of `this.emitCommand!"changetab"();`.
6795 
6796 	History:
6797 		Added December 24, 2021 (dub v10.5)
6798 +/
6799 class TabMessageWidget : Widget {
6800 
6801 	protected void tabIndexClicked(int item) {
6802 		this.emitCommand!"changetab"();
6803 	}
6804 
6805 	/++
6806 		Adds the a new tab to the control with the given title.
6807 
6808 		Returns:
6809 			The index of the newly added tab. You will need to know
6810 			this index to refer to it later and to know which tab to
6811 			change to when you get a changetab message.
6812 	+/
6813 	int addTab(string title, int pos = int.max) {
6814 		version(win32_widgets) {
6815 			TCITEM item;
6816 			item.mask = TCIF_TEXT;
6817 			WCharzBuffer buf = WCharzBuffer(title);
6818 			item.pszText = buf.ptr;
6819 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
6820 		} else version(custom_widgets) {
6821 			if(pos >= tabs.length) {
6822 				tabs ~= title;
6823 				redraw();
6824 				return cast(int) tabs.length - 1;
6825 			} else if(pos <= 0) {
6826 				tabs = title ~ tabs;
6827 				redraw();
6828 				return 0;
6829 			} else {
6830 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
6831 				redraw();
6832 				return pos;
6833 			}
6834 		}
6835 	}
6836 
6837 	override void addChild(Widget child, int pos = int.max) {
6838 		if(container)
6839 			container.addChild(child, pos);
6840 		else
6841 			super.addChild(child, pos);
6842 	}
6843 
6844 	protected Widget makeContainer() {
6845 		return new Widget(this);
6846 	}
6847 
6848 	private Widget container;
6849 
6850 	override void recomputeChildLayout() {
6851 		version(win32_widgets) {
6852 			this.registerMovement();
6853 
6854 			RECT rect;
6855 			GetWindowRect(hwnd, &rect);
6856 
6857 			auto left = rect.left;
6858 			auto top = rect.top;
6859 
6860 			TabCtrl_AdjustRect(hwnd, false, &rect);
6861 			foreach(child; children) {
6862 				if(!child.showing) continue;
6863 				child.x = rect.left - left;
6864 				child.y = rect.top - top;
6865 				child.width = rect.right - rect.left;
6866 				child.height = rect.bottom - rect.top;
6867 				child.recomputeChildLayout();
6868 			}
6869 		} else version(custom_widgets) {
6870 			this.registerMovement();
6871 			foreach(child; children) {
6872 				if(!child.showing) continue;
6873 				child.x = 2;
6874 				child.y = tabBarHeight + 2; // for the border
6875 				child.width = width - 4; // for the border
6876 				child.height = height - tabBarHeight - 2 - 2; // for the border
6877 				child.recomputeChildLayout();
6878 			}
6879 		} else static assert(0);
6880 	}
6881 
6882 	version(custom_widgets)
6883 		string[] tabs;
6884 
6885 	this(Widget parent) {
6886 		super(parent);
6887 
6888 		tabStop = false;
6889 
6890 		version(win32_widgets) {
6891 			createWin32Window(this, WC_TABCONTROL, "", 0);
6892 		} else version(custom_widgets) {
6893 			addEventListener((ClickEvent event) {
6894 				if(event.target !is this)
6895 					return;
6896 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
6897 					auto t = (event.clientX / tabWidth);
6898 					if(t >= 0 && t < tabs.length) {
6899 						currentTab_ = t;
6900 						tabIndexClicked(t);
6901 						redraw();
6902 					}
6903 				}
6904 			});
6905 		} else static assert(0);
6906 
6907 		this.container = makeContainer();
6908 	}
6909 
6910 	override int marginTop() { return 4; }
6911 	override int paddingBottom() { return 4; }
6912 
6913 	override int minHeight() {
6914 		int max = 0;
6915 		foreach(child; children)
6916 			max = mymax(child.minHeight, max);
6917 
6918 
6919 		version(win32_widgets) {
6920 			RECT rect;
6921 			rect.right = this.width;
6922 			rect.bottom = max;
6923 			TabCtrl_AdjustRect(hwnd, true, &rect);
6924 
6925 			max = rect.bottom;
6926 		} else {
6927 			max += defaultLineHeight + 4;
6928 		}
6929 
6930 
6931 		return max;
6932 	}
6933 
6934 	version(win32_widgets)
6935 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
6936 		switch(code) {
6937 			case TCN_SELCHANGE:
6938 				auto sel = TabCtrl_GetCurSel(hwnd);
6939 				tabIndexClicked(sel);
6940 			break;
6941 			default:
6942 		}
6943 		return 0;
6944 	}
6945 
6946 	version(custom_widgets) {
6947 		private int currentTab_;
6948 		private int tabBarHeight() { return defaultLineHeight; }
6949 		int tabWidth() { return scaleWithDpi(80); }
6950 	}
6951 
6952 	version(win32_widgets)
6953 	override void paint(WidgetPainter painter) {}
6954 
6955 	version(custom_widgets)
6956 	override void paint(WidgetPainter painter) {
6957 		auto cs = getComputedStyle();
6958 
6959 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
6960 
6961 		int posX = 0;
6962 		foreach(idx, title; tabs) {
6963 			auto isCurrent = idx == getCurrentTab();
6964 
6965 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
6966 
6967 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
6968 			painter.outlineColor = cs.foregroundColor;
6969 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
6970 
6971 			if(isCurrent) {
6972 				painter.outlineColor = cs.windowBackgroundColor;
6973 				painter.fillColor = Color.transparent;
6974 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
6975 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
6976 
6977 				painter.outlineColor = Color.white;
6978 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
6979 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
6980 				painter.outlineColor = cs.activeTabColor;
6981 				painter.drawPixel(Point(posX, tabBarHeight - 1));
6982 			}
6983 
6984 			posX += tabWidth - 2;
6985 		}
6986 	}
6987 
6988 	///
6989 	@scriptable
6990 	void setCurrentTab(int item) {
6991 		version(win32_widgets)
6992 			TabCtrl_SetCurSel(hwnd, item);
6993 		else version(custom_widgets)
6994 			currentTab_ = item;
6995 		else static assert(0);
6996 
6997 		tabIndexClicked(item);
6998 	}
6999 
7000 	///
7001 	@scriptable
7002 	int getCurrentTab() {
7003 		version(win32_widgets)
7004 			return TabCtrl_GetCurSel(hwnd);
7005 		else version(custom_widgets)
7006 			return currentTab_; // FIXME
7007 		else static assert(0);
7008 	}
7009 
7010 	///
7011 	@scriptable
7012 	void removeTab(int item) {
7013 		if(item && item == getCurrentTab())
7014 			setCurrentTab(item - 1);
7015 
7016 		version(win32_widgets) {
7017 			TabCtrl_DeleteItem(hwnd, item);
7018 		}
7019 
7020 		for(int a = item; a < children.length - 1; a++)
7021 			this._children[a] = this._children[a + 1];
7022 		this._children = this._children[0 .. $-1];
7023 	}
7024 
7025 }
7026 
7027 
7028 /++
7029 	A tab widget is a set of clickable tab buttons followed by a content area.
7030 
7031 
7032 	Tabs can change existing content or can be new pages.
7033 
7034 	When the user picks a different tab, a `change` message is generated.
7035 +/
7036 class TabWidget : TabMessageWidget {
7037 	this(Widget parent) {
7038 		super(parent);
7039 	}
7040 
7041 	override protected Widget makeContainer() {
7042 		return null;
7043 	}
7044 
7045 	override void addChild(Widget child, int pos = int.max) {
7046 		if(auto twp = cast(TabWidgetPage) child) {
7047 			Widget.addChild(child, pos);
7048 			if(pos == int.max)
7049 				pos = cast(int) this.children.length - 1;
7050 
7051 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7052 
7053 			if(pos != getCurrentTab) {
7054 				child.showing = false;
7055 			}
7056 		} else {
7057 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7058 		}
7059 	}
7060 
7061 	// FIXME: add tab icons at some point, Windows supports them
7062 	/++
7063 		Adds a page and its associated tab with the given label to the widget.
7064 
7065 		Returns:
7066 			The added page object, to which you can add other widgets.
7067 	+/
7068 	@scriptable
7069 	TabWidgetPage addPage(string title) {
7070 		return new TabWidgetPage(title, this);
7071 	}
7072 
7073 	/++
7074 		Gets the page at the given tab index, or `null` if the index is bad.
7075 
7076 		History:
7077 			Added December 24, 2021.
7078 	+/
7079 	TabWidgetPage getPage(int index) {
7080 		if(index < this.children.length)
7081 			return null;
7082 		return cast(TabWidgetPage) this.children[index];
7083 	}
7084 
7085 	/++
7086 		While you can still use the addTab from the parent class,
7087 		*strongly* recommend you use [addPage] insteaad.
7088 
7089 		History:
7090 			Added December 24, 2021 to fulful the interface
7091 			requirement that came from adding [TabMessageWidget].
7092 
7093 			You should not use it though since the [addPage] function
7094 			is much easier to use here.
7095 	+/
7096 	override int addTab(string title, int pos = int.max) {
7097 		auto p = addPage(title);
7098 		foreach(idx, child; this.children)
7099 			if(child is p)
7100 				return cast(int) idx;
7101 		return -1;
7102 	}
7103 
7104 	protected override void tabIndexClicked(int item) {
7105 		foreach(idx, child; children) {
7106 			child.showing(false, false); // batch the recalculates for the end
7107 		}
7108 
7109 		foreach(idx, child; children) {
7110 			if(idx == item) {
7111 				child.showing(true, false);
7112 				if(parentWindow) {
7113 					auto f = parentWindow.getFirstFocusable(child);
7114 					if(f)
7115 						f.focus();
7116 				}
7117 				recomputeChildLayout();
7118 			}
7119 		}
7120 
7121 		version(win32_widgets) {
7122 			InvalidateRect(hwnd, null, true);
7123 		} else version(custom_widgets) {
7124 			this.redraw();
7125 		}
7126 	}
7127 
7128 }
7129 
7130 /++
7131 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7132 
7133 	You add [TabWidgetPage]s to it.
7134 +/
7135 class PageWidget : Widget {
7136 	this(Widget parent) {
7137 		super(parent);
7138 	}
7139 
7140 	override int minHeight() {
7141 		int max = 0;
7142 		foreach(child; children)
7143 			max = mymax(child.minHeight, max);
7144 
7145 		return max;
7146 	}
7147 
7148 
7149 	override void addChild(Widget child, int pos = int.max) {
7150 		if(auto twp = cast(TabWidgetPage) child) {
7151 			super.addChild(child, pos);
7152 			if(pos == int.max)
7153 				pos = cast(int) this.children.length - 1;
7154 
7155 			if(pos != getCurrentTab) {
7156 				child.showing = false;
7157 			}
7158 		} else {
7159 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7160 		}
7161 	}
7162 
7163 	override void recomputeChildLayout() {
7164 		this.registerMovement();
7165 		foreach(child; children) {
7166 			child.x = 0;
7167 			child.y = 0;
7168 			child.width = width;
7169 			child.height = height;
7170 			child.recomputeChildLayout();
7171 		}
7172 	}
7173 
7174 	private int currentTab_;
7175 
7176 	///
7177 	@scriptable
7178 	void setCurrentTab(int item) {
7179 		currentTab_ = item;
7180 
7181 		showOnly(item);
7182 	}
7183 
7184 	///
7185 	@scriptable
7186 	int getCurrentTab() {
7187 		return currentTab_;
7188 	}
7189 
7190 	///
7191 	@scriptable
7192 	void removeTab(int item) {
7193 		if(item && item == getCurrentTab())
7194 			setCurrentTab(item - 1);
7195 
7196 		for(int a = item; a < children.length - 1; a++)
7197 			this._children[a] = this._children[a + 1];
7198 		this._children = this._children[0 .. $-1];
7199 	}
7200 
7201 	///
7202 	@scriptable
7203 	TabWidgetPage addPage(string title) {
7204 		return new TabWidgetPage(title, this);
7205 	}
7206 
7207 	private void showOnly(int item) {
7208 		foreach(idx, child; children)
7209 			if(idx == item) {
7210 				child.show();
7211 				child.queueRecomputeChildLayout();
7212 			} else {
7213 				child.hide();
7214 			}
7215 	}
7216 
7217 }
7218 
7219 /++
7220 
7221 +/
7222 class TabWidgetPage : Widget {
7223 	string title;
7224 	this(string title, Widget parent) {
7225 		this.title = title;
7226 		this.tabStop = false;
7227 		super(parent);
7228 
7229 		///*
7230 		version(win32_widgets) {
7231 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7232 		}
7233 		//*/
7234 	}
7235 
7236 	override int minHeight() {
7237 		int sum = 0;
7238 		foreach(child; children)
7239 			sum += child.minHeight();
7240 		return sum;
7241 	}
7242 }
7243 
7244 version(none)
7245 /++
7246 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7247 
7248 	I think I need to modify the layout algorithms to support this.
7249 +/
7250 class CollapsableSidebar : Widget {
7251 
7252 }
7253 
7254 /// Stacks the widgets vertically, taking all the available width for each child.
7255 class VerticalLayout : Layout {
7256 	// most of this is intentionally blank - widget's default is vertical layout right now
7257 	///
7258 	this(Widget parent) { super(parent); }
7259 
7260 	/++
7261 		Sets a max width for the layout so you don't have to subclass. The max width
7262 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7263 
7264 		History:
7265 			Added November 29, 2021 (dub v10.5)
7266 	+/
7267 	this(int maxWidth, Widget parent) {
7268 		this.mw = maxWidth;
7269 		super(parent);
7270 	}
7271 
7272 	private int mw = int.max;
7273 
7274 	override int maxWidth() { return scaleWithDpi(mw); }
7275 }
7276 
7277 /// Stacks the widgets horizontally, taking all the available height for each child.
7278 class HorizontalLayout : Layout {
7279 	///
7280 	this(Widget parent) { super(parent); }
7281 
7282 	/++
7283 		Sets a max height for the layout so you don't have to subclass. The max height
7284 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7285 
7286 		History:
7287 			Added November 29, 2021 (dub v10.5)
7288 	+/
7289 	this(int maxHeight, Widget parent) {
7290 		this.mh = maxHeight;
7291 		super(parent);
7292 	}
7293 
7294 	private int mh = 0;
7295 
7296 
7297 
7298 	override void recomputeChildLayout() {
7299 		.recomputeChildLayout!"width"(this);
7300 	}
7301 
7302 	override int minHeight() {
7303 		int largest = 0;
7304 		int margins = 0;
7305 		int lastMargin = 0;
7306 		foreach(child; children) {
7307 			auto mh = child.minHeight();
7308 			if(mh > largest)
7309 				largest = mh;
7310 			margins += mymax(lastMargin, child.marginTop());
7311 			lastMargin = child.marginBottom();
7312 		}
7313 		return largest + margins;
7314 	}
7315 
7316 	override int maxHeight() {
7317 		if(mh != 0)
7318 			return mymax(minHeight, scaleWithDpi(mh));
7319 
7320 		int largest = 0;
7321 		int margins = 0;
7322 		int lastMargin = 0;
7323 		foreach(child; children) {
7324 			auto mh = child.maxHeight();
7325 			if(mh == int.max)
7326 				return int.max;
7327 			if(mh > largest)
7328 				largest = mh;
7329 			margins += mymax(lastMargin, child.marginTop());
7330 			lastMargin = child.marginBottom();
7331 		}
7332 		return largest + margins;
7333 	}
7334 
7335 	override int heightStretchiness() {
7336 		int max;
7337 		foreach(child; children) {
7338 			auto c = child.heightStretchiness;
7339 			if(c > max)
7340 				max = c;
7341 		}
7342 		return max;
7343 	}
7344 
7345 }
7346 
7347 version(win32_widgets)
7348 private
7349 extern(Windows)
7350 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
7351 	Widget* pwin = hwnd in Widget.nativeMapping;
7352 	if(pwin is null)
7353 		return DefWindowProc(hwnd, message, wparam, lparam);
7354 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
7355 	if(win is null)
7356 		return DefWindowProc(hwnd, message, wparam, lparam);
7357 
7358 	switch(message) {
7359 		case WM_SIZE:
7360 			auto width = LOWORD(lparam);
7361 			auto height = HIWORD(lparam);
7362 
7363 			auto hdc = GetDC(hwnd);
7364 			auto hdcBmp = CreateCompatibleDC(hdc);
7365 
7366 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
7367 			if(width > win.bmpWidth || height > win.bmpHeight) {
7368 				auto oldBuffer = win.buffer;
7369 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
7370 
7371 				if(oldBuffer)
7372 					DeleteObject(oldBuffer);
7373 
7374 				win.bmpWidth = width;
7375 				win.bmpHeight = height;
7376 			}
7377 
7378 			// just always erase it upon resizing so minigui can draw over with a clean slate
7379 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
7380 
7381 			auto brush = GetSysColorBrush(COLOR_3DFACE);
7382 			RECT r;
7383 			r.left = 0;
7384 			r.top = 0;
7385 			r.right = width;
7386 			r.bottom = height;
7387 			FillRect(hdcBmp, &r, brush);
7388 
7389 			SelectObject(hdcBmp, oldBmp);
7390 			DeleteDC(hdcBmp);
7391 			ReleaseDC(hwnd, hdc);
7392 		break;
7393 		case WM_PAINT:
7394 			if(win.buffer is null)
7395 				goto default;
7396 
7397 			BITMAP bm;
7398 			PAINTSTRUCT ps;
7399 
7400 			HDC hdc = BeginPaint(hwnd, &ps);
7401 
7402 			HDC hdcMem = CreateCompatibleDC(hdc);
7403 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
7404 
7405 			GetObject(win.buffer, bm.sizeof, &bm);
7406 
7407 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
7408 
7409 			SelectObject(hdcMem, hbmOld);
7410 			DeleteDC(hdcMem);
7411 			EndPaint(hwnd, &ps);
7412 		break;
7413 		default:
7414 			return DefWindowProc(hwnd, message, wparam, lparam);
7415 	}
7416 
7417 	return 0;
7418 }
7419 
7420 private wstring Win32Class(wstring name)() {
7421 	static bool classRegistered;
7422 	if(!classRegistered) {
7423 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
7424 		WNDCLASSEX wc;
7425 		wc.cbSize = wc.sizeof;
7426 		wc.hInstance = hInstance;
7427 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
7428 		wc.lpfnWndProc = &DoubleBufferWndProc;
7429 		wc.lpszClassName = name.ptr;
7430 		if(!RegisterClassExW(&wc))
7431 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
7432 		classRegistered = true;
7433 	}
7434 
7435 		return name;
7436 }
7437 
7438 /+
7439 version(win32_widgets)
7440 extern(Windows)
7441 private
7442 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
7443 	switch(iMessage) {
7444 		case WM_PAINT:
7445 			if(auto te = hWnd in Widget.nativeMapping) {
7446 				try {
7447 					//te.redraw();
7448 					writeln(te, " drawing");
7449 				} catch(Exception) {}
7450 			}
7451 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7452 		default:
7453 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7454 	}
7455 }
7456 +/
7457 
7458 
7459 /++
7460 	A widget specifically designed to hold other widgets.
7461 
7462 	History:
7463 		Added July 1, 2021
7464 +/
7465 class ContainerWidget : Widget {
7466 	this(Widget parent) {
7467 		super(parent);
7468 		this.tabStop = false;
7469 
7470 		version(win32_widgets) {
7471 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
7472 		}
7473 	}
7474 }
7475 
7476 /++
7477 	A widget that takes your widget, puts scroll bars around it, and sends
7478 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
7479 	no effort to automatically scroll or clip its child widgets - it just sends
7480 	the messages.
7481 
7482 
7483 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
7484 	The scroll coordinates are all given in a unit you interpret as you wish. One
7485 	of these units is moved on each press of the arrow buttons and represents the
7486 	smallest amount the user can scroll. The intention is for this to be one line,
7487 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
7488 	in each direction that the user might be interested in.
7489 
7490 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
7491 	This is the amount it jumps when the user pressed page up and page down, or clicks
7492 	in the exposed part of the scroll bar.
7493 
7494 	You should add child content to the ScrollMessageWidget. However, it is important to
7495 	note that the coordinates are always independent of the scroll position! It is YOUR
7496 	responsibility to do any necessary transforms, clipping, etc., while drawing the
7497 	content and interpreting mouse events if they are supposed to change with the scroll.
7498 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
7499 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
7500 	you more control (which can be considerably more efficient and adapted to your actual data)
7501 	at the expense of you also needing to be aware of its reality.
7502 
7503 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
7504 	version 10.3. Maybe this will change in the future.... but for now you must call
7505 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
7506 +/
7507 class ScrollMessageWidget : Widget {
7508 	this(Widget parent) {
7509 		super(parent);
7510 
7511 		container = new Widget(this);
7512 		hsb = new HorizontalScrollbar(this);
7513 		vsb = new VerticalScrollbar(this);
7514 
7515 		hsb.addEventListener("scrolltonextline", {
7516 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
7517 			notify();
7518 		});
7519 		hsb.addEventListener("scrolltopreviousline", {
7520 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
7521 			notify();
7522 		});
7523 		vsb.addEventListener("scrolltonextline", {
7524 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
7525 			notify();
7526 		});
7527 		vsb.addEventListener("scrolltopreviousline", {
7528 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
7529 			notify();
7530 		});
7531 		hsb.addEventListener("scrolltonextpage", {
7532 			hsb.setPosition(hsb.position + hsb.step_);
7533 			notify();
7534 		});
7535 		hsb.addEventListener("scrolltopreviouspage", {
7536 			hsb.setPosition(hsb.position - hsb.step_);
7537 			notify();
7538 		});
7539 		vsb.addEventListener("scrolltonextpage", {
7540 			vsb.setPosition(vsb.position + vsb.step_);
7541 			notify();
7542 		});
7543 		vsb.addEventListener("scrolltopreviouspage", {
7544 			vsb.setPosition(vsb.position - vsb.step_);
7545 			notify();
7546 		});
7547 		hsb.addEventListener("scrolltoposition", (Event event) {
7548 			hsb.setPosition(event.intValue);
7549 			notify();
7550 		});
7551 		vsb.addEventListener("scrolltoposition", (Event event) {
7552 			vsb.setPosition(event.intValue);
7553 			notify();
7554 		});
7555 
7556 
7557 		tabStop = false;
7558 		container.tabStop = false;
7559 		magic = true;
7560 	}
7561 
7562 	private int movementPerButtonClickH_ = 1;
7563 	private int movementPerButtonClickV_ = 1;
7564 	public void movementPerButtonClick(int h, int v) {
7565 		movementPerButtonClickH_ = h;
7566 		movementPerButtonClickV_ = v;
7567 	}
7568 
7569 	/++
7570 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
7571 
7572 
7573 		The defaults for [addDefaultWheelListeners] are:
7574 
7575 			$(LIST
7576 				* Mouse wheel scrolls vertically
7577 				* Alt key + mouse wheel scrolls horiontally
7578 				* Shift + mouse wheel scrolls faster.
7579 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
7580 			)
7581 
7582 		The defaults for [addDefaultKeyboardListeners] are:
7583 
7584 			$(LIST
7585 				* Arrow keys scroll by the given amounts
7586 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
7587 				* Page up and down scroll by the vertical viewable area
7588 				* Home and end scroll to the start and end of the verticle viewable area.
7589 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
7590 			)
7591 
7592 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
7593 
7594 		Params:
7595 			horizontalArrowScrollAmount =
7596 			verticalArrowScrollAmount =
7597 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
7598 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
7599 			shiftMultiplier = multiplies the scroll amount by this when shift is held
7600 	+/
7601 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
7602 		auto _this = this;
7603 
7604 		container.addEventListener((scope KeyDownEvent ke) {
7605 			switch(ke.key) {
7606 				case Key.Left:
7607 					_this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7608 				break;
7609 				case Key.Right:
7610 					_this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7611 				break;
7612 				case Key.Up:
7613 					_this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7614 				break;
7615 				case Key.Down:
7616 					_this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7617 				break;
7618 				case Key.PageUp:
7619 					if(ke.altKey)
7620 						_this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7621 					else
7622 						_this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7623 				break;
7624 				case Key.PageDown:
7625 					if(ke.altKey)
7626 						_this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7627 					else
7628 						_this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7629 				break;
7630 				case Key.Home:
7631 					if(ke.altKey)
7632 						_this.scrollLeft(short.max * 16);
7633 					else
7634 						_this.scrollUp(short.max * 16);
7635 				break;
7636 				case Key.End:
7637 					if(ke.altKey)
7638 						_this.scrollRight(short.max * 16);
7639 					else
7640 						_this.scrollDown(short.max * 16);
7641 				break;
7642 
7643 				default:
7644 					// ignore, not for us.
7645 			}
7646 
7647 		});
7648 	}
7649 
7650 	/// ditto
7651 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
7652 		auto _this = this;
7653 		container.addEventListener((scope ClickEvent ce) {
7654 
7655 			//if(ce.target && ce.target.tabStop)
7656 				//ce.target.focus();
7657 
7658 			// ctrl is reserved for the application
7659 			if(ce.ctrlKey)
7660 				return;
7661 
7662 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
7663 				return;
7664 
7665 			if(shiftMultiplier == 0 && ce.shiftKey)
7666 				return;
7667 
7668 			if(ce.button == MouseButton.wheelDown) {
7669 				if(ce.altKey)
7670 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7671 				else
7672 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7673 			} else if(ce.button == MouseButton.wheelUp) {
7674 				if(ce.altKey)
7675 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7676 				else
7677 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7678 			}
7679 		});
7680 	}
7681 
7682 	/++
7683 		Scrolls the given amount.
7684 
7685 		History:
7686 			The scroll up and down functions was here in the initial release of the class, but the `amount` parameter and left/right functions were added on September 28, 2021.
7687 	+/
7688 	void scrollUp(int amount = 1) {
7689 		vsb.setPosition(vsb.position - amount);
7690 		notify();
7691 	}
7692 	/// ditto
7693 	void scrollDown(int amount = 1) {
7694 		vsb.setPosition(vsb.position + amount);
7695 		notify();
7696 	}
7697 	/// ditto
7698 	void scrollLeft(int amount = 1) {
7699 		hsb.setPosition(hsb.position - amount);
7700 		notify();
7701 	}
7702 	/// ditto
7703 	void scrollRight(int amount = 1) {
7704 		hsb.setPosition(hsb.position + amount);
7705 		notify();
7706 	}
7707 
7708 	///
7709 	VerticalScrollbar verticalScrollBar() { return vsb; }
7710 	///
7711 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
7712 
7713 	void notify() {
7714 		static bool insideNotify;
7715 
7716 		if(insideNotify)
7717 			return; // avoid the recursive call, even if it isn't strictly correct
7718 
7719 		insideNotify = true;
7720 		scope(exit) insideNotify = false;
7721 
7722 		this.emit!ScrollEvent();
7723 	}
7724 
7725 	mixin Emits!ScrollEvent;
7726 
7727 	///
7728 	Point position() {
7729 		return Point(hsb.position, vsb.position);
7730 	}
7731 
7732 	///
7733 	void setPosition(int x, int y) {
7734 		hsb.setPosition(x);
7735 		vsb.setPosition(y);
7736 	}
7737 
7738 	///
7739 	void setPageSize(int unitsX, int unitsY) {
7740 		hsb.setStep(unitsX);
7741 		vsb.setStep(unitsY);
7742 	}
7743 
7744 	/// Always call this BEFORE setViewableArea
7745 	void setTotalArea(int width, int height) {
7746 		hsb.setMax(width);
7747 		vsb.setMax(height);
7748 	}
7749 
7750 	/++
7751 		Always set the viewable area AFTER setitng the total area if you are going to change both.
7752 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
7753 		If you need to do that, use [queueRecomputeChildLayout].
7754 	+/
7755 	void setViewableArea(int width, int height) {
7756 
7757 		// actually there IS A need to dothis cuz the max might have changed since then
7758 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
7759 			//return; // no need to do what is already done
7760 		hsb.setViewableArea(width);
7761 		vsb.setViewableArea(height);
7762 
7763 		bool needsNotify = false;
7764 
7765 		// FIXME: if at any point the rhs is outside the scrollbar, we need
7766 		// to reset to 0. but it should remember the old position in case the
7767 		// window resizes again, so it can kinda return ot where it was.
7768 		//
7769 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
7770 		if(width >= hsb.max) {
7771 			// there's plenty of room to display it all so we need to reset to zero
7772 			// FIXME: adjust so it matches the note above
7773 			hsb.setPosition(0);
7774 			needsNotify = true;
7775 		}
7776 		if(height >= vsb.max) {
7777 			// there's plenty of room to display it all so we need to reset to zero
7778 			// FIXME: adjust so it matches the note above
7779 			vsb.setPosition(0);
7780 			needsNotify = true;
7781 		}
7782 		if(needsNotify)
7783 			notify();
7784 	}
7785 
7786 	private bool magic;
7787 	override void addChild(Widget w, int position = int.max) {
7788 		if(magic)
7789 			container.addChild(w, position);
7790 		else
7791 			super.addChild(w, position);
7792 	}
7793 
7794 	override void recomputeChildLayout() {
7795 		if(hsb is null || vsb is null || container is null) return;
7796 
7797 		registerMovement();
7798 
7799 		enum BUTTON_SIZE = 16;
7800 
7801 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
7802 		hsb.x = 0;
7803 		hsb.y = this.height - hsb.height;
7804 
7805 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
7806 		vsb.x = this.width - vsb.width;
7807 		vsb.y = 0;
7808 
7809 		auto vsb_width = vsb.showing ? vsb.width : 0;
7810 		auto hsb_height = hsb.showing ? hsb.height : 0;
7811 
7812 		hsb.width = this.width - vsb_width;
7813 		vsb.height = this.height - hsb_height;
7814 
7815 		hsb.recomputeChildLayout();
7816 		vsb.recomputeChildLayout();
7817 
7818 		if(this.header is null) {
7819 			container.x = 0;
7820 			container.y = 0;
7821 			container.width = this.width - vsb_width;
7822 			container.height = this.height - hsb_height;
7823 			container.recomputeChildLayout();
7824 		} else {
7825 			header.x = 0;
7826 			header.y = 0;
7827 			header.width = this.width - vsb_width;
7828 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
7829 			header.recomputeChildLayout();
7830 
7831 			container.x = 0;
7832 			container.y = scaleWithDpi(BUTTON_SIZE);
7833 			container.width = this.width - vsb_width;
7834 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
7835 			container.recomputeChildLayout();
7836 		}
7837 	}
7838 
7839 	private HorizontalScrollbar hsb;
7840 	private VerticalScrollbar vsb;
7841 	Widget container;
7842 	private Widget header;
7843 
7844 	/++
7845 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
7846 
7847 		History:
7848 			Added September 27, 2021 (dub v10.3)
7849 	+/
7850 	Widget getHeader() {
7851 		if(this.header is null) {
7852 			magic = false;
7853 			scope(exit) magic = true;
7854 			this.header = new Widget(this);
7855 			queueRecomputeChildLayout();
7856 		}
7857 		return this.header;
7858 	}
7859 
7860 	/++
7861 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
7862 
7863 		History:
7864 			Added January 3, 2023 (dub v11.0)
7865 	+/
7866 	void scrollIntoView(Rectangle rect) {
7867 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
7868 
7869 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
7870 
7871 		// the lower right is exclusive normally
7872 		auto test = rect.lowerRight;
7873 		if(test.x > 0) test.x--;
7874 		if(test.y > 0) test.y--;
7875 
7876 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
7877 			// try to scroll only one dimension at a time if we can
7878 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
7879 				setPosition(rect.upperLeft.x, position.y);
7880 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
7881 				setPosition(position.x, rect.upperLeft.y);
7882 		}
7883 
7884 	}
7885 
7886 	override int minHeight() {
7887 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
7888 		if(header !is null)
7889 			min += header.minHeight;
7890 		if(horizontalScrollBar.showing)
7891 			min += horizontalScrollBar.minHeight;
7892 		return min;
7893 	}
7894 
7895 	override int maxHeight() {
7896 		int max = container ? container.maxHeight : int.max;
7897 		if(max == int.max)
7898 			return max;
7899 		if(horizontalScrollBar.showing)
7900 			max += horizontalScrollBar.minHeight;
7901 		return max;
7902 	}
7903 }
7904 
7905 /++
7906 	$(IMG //arsdnet.net/minigui-screenshots/windows/ScrollMessageWidget.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep")
7907 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
7908 +/
7909 version(minigui_screenshots)
7910 @Screenshot("ScrollMessageWidget")
7911 unittest {
7912 	auto window = new Window("ScrollMessageWidget");
7913 
7914 	auto smw = new ScrollMessageWidget(window);
7915 	smw.addDefaultKeyboardListeners();
7916 	smw.addDefaultWheelListeners();
7917 
7918 	window.loop();
7919 }
7920 
7921 /++
7922 	Bypasses automatic layout for its children, using manual positioning and sizing only.
7923 	While you need to manually position them, you must ensure they are inside the StaticLayout's
7924 	bounding box to avoid undefined behavior.
7925 
7926 	You should almost never use this.
7927 +/
7928 class StaticLayout : Layout {
7929 	///
7930 	this(Widget parent) { super(parent); }
7931 	override void recomputeChildLayout() {
7932 		registerMovement();
7933 		foreach(child; children)
7934 			child.recomputeChildLayout();
7935 	}
7936 }
7937 
7938 /++
7939 	Bypasses automatic positioning when being laid out. It is your responsibility to make
7940 	room for this widget in the parent layout.
7941 
7942 	Its children are laid out normally, unless there is exactly one, in which case it takes
7943 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
7944 	can do that with `padding`).
7945 +/
7946 class StaticPosition : Layout {
7947 	///
7948 	this(Widget parent) { super(parent); }
7949 
7950 	override void recomputeChildLayout() {
7951 		registerMovement();
7952 		if(this.children.length == 1) {
7953 			auto child = children[0];
7954 			child.x = 0;
7955 			child.y = 0;
7956 			child.width = this.width;
7957 			child.height = this.height;
7958 			child.recomputeChildLayout();
7959 		} else
7960 		foreach(child; children)
7961 			child.recomputeChildLayout();
7962 	}
7963 
7964 	alias width = typeof(super).width;
7965 	alias height = typeof(super).height;
7966 
7967 	@property int width(int w) @nogc pure @safe nothrow {
7968 		return this._width = w;
7969 	}
7970 
7971 	@property int height(int w) @nogc pure @safe nothrow {
7972 		return this._height = w;
7973 	}
7974 
7975 }
7976 
7977 /++
7978 	FixedPosition is like [StaticPosition], but its coordinates
7979 	are always relative to the viewport, meaning they do not scroll with
7980 	the parent content.
7981 +/
7982 class FixedPosition : StaticPosition {
7983 	///
7984 	this(Widget parent) { super(parent); }
7985 }
7986 
7987 version(win32_widgets)
7988 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
7989 	if(true) {
7990 		// cmd == 0 = menu, cmd == 1 = accelerator
7991 		if(auto item = idm in Action.mapping) {
7992 			foreach(handler; (*item).triggered)
7993 				handler();
7994 		/*
7995 			auto event = new Event("triggered", *item);
7996 			event.button = idm;
7997 			event.dispatch();
7998 		*/
7999 			return 0;
8000 		}
8001 	}
8002 	if(handle)
8003 	if(auto widgetp = handle in Widget.nativeMapping) {
8004 		(*widgetp).handleWmCommand(cmd, idm);
8005 		return 0;
8006 	}
8007 	return 1;
8008 }
8009 
8010 
8011 ///
8012 class Window : Widget {
8013 	int mouseCaptureCount = 0;
8014 	Widget mouseCapturedBy;
8015 	void captureMouse(Widget byWhom) {
8016 		assert(mouseCapturedBy is null || byWhom is mouseCapturedBy);
8017 		mouseCaptureCount++;
8018 		mouseCapturedBy = byWhom;
8019 		win.grabInput();
8020 	}
8021 	void releaseMouseCapture() {
8022 		mouseCaptureCount--;
8023 		mouseCapturedBy = null;
8024 		win.releaseInputGrab();
8025 	}
8026 
8027 	/++
8028 		Sets the window icon which is often seen in title bars and taskbars.
8029 
8030 		History:
8031 			Added April 5, 2022 (dub v10.8)
8032 	+/
8033 	@property void icon(MemoryImage icon) {
8034 		if(win && icon)
8035 			win.icon = icon;
8036 	}
8037 
8038 	// forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples
8039 	// this does NOT change the icon on the window! That's what the other overload is for
8040 	static @property .icon icon(GenericIcons i) {
8041 		return .icon(i);
8042 	}
8043 
8044 	///
8045 	@scriptable
8046 	@property bool focused() {
8047 		return win.focused;
8048 	}
8049 
8050 	static class Style : Widget.Style {
8051 		override WidgetBackground background() {
8052 			version(custom_widgets)
8053 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8054 			else version(win32_widgets)
8055 				return WidgetBackground(Color.transparent);
8056 			else static assert(0);
8057 		}
8058 	}
8059 	mixin OverrideStyle!Style;
8060 
8061 	/++
8062 		Gives the height of a line according to the default font. You should try to use your computed font instead of this, but until May 8, 2021, this was the only real option.
8063 	+/
8064 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8065 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8066 	}
8067 
8068 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8069 		OperatingSystemFont font;
8070 		if(auto vt = WidgetPainter.visualTheme) {
8071 			font = vt.defaultFontCached(96); // FIXME
8072 		}
8073 
8074 		if(font is null) {
8075 			static int defaultHeightCache;
8076 			if(defaultHeightCache == 0) {
8077 				font = new OperatingSystemFont;
8078 				font.loadDefault;
8079 				defaultHeightCache = font.height();// * 5 / 4;
8080 			}
8081 			return defaultHeightCache;
8082 		}
8083 
8084 		return font.height();// * 5 / 4;
8085 	}
8086 
8087 	Widget focusedWidget;
8088 
8089 	private SimpleWindow win_;
8090 
8091 	@property {
8092 		/++
8093 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8094 
8095 			History:
8096 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8097 		+/
8098 		public SimpleWindow win() {
8099 			return win_;
8100 		}
8101 		///
8102 		protected void win(SimpleWindow w) {
8103 			win_ = w;
8104 		}
8105 	}
8106 
8107 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8108 	this(Widget p) {
8109 		tabStop = false;
8110 		super(p);
8111 	}
8112 
8113 	private void actualRedraw() {
8114 		if(recomputeChildLayoutRequired)
8115 			recomputeChildLayoutEntry();
8116 		if(!showing) return;
8117 
8118 		assert(parentWindow !is null);
8119 
8120 		auto w = drawableWindow;
8121 		if(w is null)
8122 			w = parentWindow.win;
8123 
8124 		if(w.closed())
8125 			return;
8126 
8127 		auto ugh = this.parent;
8128 		int lox, loy;
8129 		while(ugh) {
8130 			lox += ugh.x;
8131 			loy += ugh.y;
8132 			ugh = ugh.parent;
8133 		}
8134 		auto painter = w.draw(true);
8135 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8136 	}
8137 
8138 
8139 	private bool skipNextChar = false;
8140 
8141 	/++
8142 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8143 
8144 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8145 	+/
8146 	this(SimpleWindow win) {
8147 
8148 		static if(UsingSimpledisplayX11) {
8149 			win.discardAdditionalConnectionState = &discardXConnectionState;
8150 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8151 		}
8152 
8153 		tabStop = false;
8154 		super(null);
8155 		this.win = win;
8156 
8157 		win.addEventListener((Widget.RedrawEvent) {
8158 			if(win.eventQueued!RecomputeEvent) {
8159 				// writeln("skipping");
8160 				return; // let the recompute event do the actual redraw
8161 			}
8162 			this.actualRedraw();
8163 		});
8164 
8165 		win.addEventListener((Widget.RecomputeEvent) {
8166 			recomputeChildLayoutEntry();
8167 			if(win.eventQueued!RedrawEvent)
8168 				return; // let the queued one do it
8169 			else {
8170 				// writeln("drawing");
8171 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8172 			}
8173 		});
8174 
8175 		this.width = win.width;
8176 		this.height = win.height;
8177 		this.parentWindow = this;
8178 
8179 		win.closeQuery = () {
8180 			if(this.emit!ClosingEvent())
8181 				win.close();
8182 		};
8183 		win.onClosing = () {
8184 			this.emit!ClosedEvent();
8185 		};
8186 
8187 		win.windowResized = (int w, int h) {
8188 			this.width = w;
8189 			this.height = h;
8190 			queueRecomputeChildLayout();
8191 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8192 			//version(win32_widgets)
8193 				//InvalidateRect(hwnd, null, true);
8194 			redraw();
8195 		};
8196 
8197 		win.onFocusChange = (bool getting) {
8198 			if(this.focusedWidget) {
8199 				if(getting) {
8200 					this.focusedWidget.emit!FocusEvent();
8201 					this.focusedWidget.emit!FocusInEvent();
8202 				} else {
8203 					this.focusedWidget.emit!BlurEvent();
8204 					this.focusedWidget.emit!FocusOutEvent();
8205 				}
8206 			}
8207 
8208 			if(getting) {
8209 				this.emit!FocusEvent();
8210 				this.emit!FocusInEvent();
8211 			} else {
8212 				this.emit!BlurEvent();
8213 				this.emit!FocusOutEvent();
8214 			}
8215 		};
8216 
8217 		win.onDpiChanged = {
8218 			this.queueRecomputeChildLayout();
8219 			auto event = new DpiChangedEvent(this);
8220 			event.sendDirectly();
8221 
8222 			privateDpiChanged();
8223 		};
8224 
8225 		win.setEventHandlers(
8226 			(MouseEvent e) {
8227 				dispatchMouseEvent(e);
8228 			},
8229 			(KeyEvent e) {
8230 				//writefln("%x   %s", cast(uint) e.key, e.key);
8231 				dispatchKeyEvent(e);
8232 			},
8233 			(dchar e) {
8234 				if(e == 13) e = 10; // hack?
8235 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8236 				dispatchCharEvent(e);
8237 			},
8238 		);
8239 
8240 		addEventListener("char", (Widget, Event ev) {
8241 			if(skipNextChar) {
8242 				ev.preventDefault();
8243 				skipNextChar = false;
8244 			}
8245 		});
8246 
8247 		version(win32_widgets)
8248 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
8249 			if(hwnd !is this.win.impl.hwnd)
8250 				return 1; // we don't care... pass it on
8251 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
8252 			if(mustReturn)
8253 				return ret;
8254 			return 1; // pass it on
8255 		};
8256 
8257 		if(Window.newWindowCreated)
8258 			Window.newWindowCreated(this);
8259 	}
8260 
8261 	version(custom_widgets)
8262 	override void defaultEventHandler_click(ClickEvent event) {
8263 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
8264 			if(event.target && event.target.tabStop)
8265 				event.target.focus();
8266 		}
8267 	}
8268 
8269 	private static void delegate(Window) newWindowCreated;
8270 
8271 	version(win32_widgets)
8272 	override void paint(WidgetPainter painter) {
8273 		/*
8274 		RECT rect;
8275 		rect.right = this.width;
8276 		rect.bottom = this.height;
8277 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
8278 		*/
8279 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
8280 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
8281 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
8282 		// since the pen is null, to fill the whole space, we need the +1 on both.
8283 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
8284 		SelectObject(painter.impl.hdc, p);
8285 		SelectObject(painter.impl.hdc, b);
8286 	}
8287 	version(custom_widgets)
8288 	override void paint(WidgetPainter painter) {
8289 		auto cs = getComputedStyle();
8290 		painter.fillColor = cs.windowBackgroundColor;
8291 		painter.outlineColor = cs.windowBackgroundColor;
8292 		painter.drawRectangle(Point(0, 0), this.width, this.height);
8293 	}
8294 
8295 
8296 	override void defaultEventHandler_keydown(KeyDownEvent event) {
8297 		Widget _this = event.target;
8298 
8299 		if(event.key == Key.Tab) {
8300 			/* Window tab ordering is a recursive thingy with each group */
8301 
8302 			// FIXME inefficient
8303 			Widget[] helper(Widget p) {
8304 				if(p.hidden)
8305 					return null;
8306 				Widget[] childOrdering;
8307 
8308 				auto children = p.children.dup;
8309 
8310 				while(true) {
8311 					// UIs should be generally small, so gonna brute force it a little
8312 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
8313 
8314 					Widget smallestTab;
8315 					foreach(ref c; children) {
8316 						if(c is null) continue;
8317 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
8318 							smallestTab = c;
8319 							c = null;
8320 						}
8321 					}
8322 					if(smallestTab !is null) {
8323 						if(smallestTab.tabStop && !smallestTab.hidden)
8324 							childOrdering ~= smallestTab;
8325 						if(!smallestTab.hidden)
8326 							childOrdering ~= helper(smallestTab);
8327 					} else
8328 						break;
8329 
8330 				}
8331 
8332 				return childOrdering;
8333 			}
8334 
8335 			Widget[] tabOrdering = helper(this);
8336 
8337 			Widget recipient;
8338 
8339 			if(tabOrdering.length) {
8340 				bool seenThis = false;
8341 				Widget previous;
8342 				foreach(idx, child; tabOrdering) {
8343 					if(child is focusedWidget) {
8344 
8345 						if(event.shiftKey) {
8346 							if(idx == 0)
8347 								recipient = tabOrdering[$-1];
8348 							else
8349 								recipient = tabOrdering[idx - 1];
8350 							break;
8351 						}
8352 
8353 						seenThis = true;
8354 						if(idx + 1 == tabOrdering.length) {
8355 							// we're at the end, either move to the next group
8356 							// or start back over
8357 							recipient = tabOrdering[0];
8358 						}
8359 						continue;
8360 					}
8361 					if(seenThis) {
8362 						recipient = child;
8363 						break;
8364 					}
8365 					previous = child;
8366 				}
8367 			}
8368 
8369 			if(recipient !is null) {
8370 				//  writeln(typeid(recipient));
8371 				recipient.focus();
8372 
8373 				skipNextChar = true;
8374 			}
8375 		}
8376 
8377 		debug if(event.key == Key.F12) {
8378 			if(devTools) {
8379 				devTools.close();
8380 				devTools = null;
8381 			} else {
8382 				devTools = new DevToolWindow(this);
8383 				devTools.show();
8384 			}
8385 		}
8386 	}
8387 
8388 	debug DevToolWindow devTools;
8389 
8390 
8391 	/++
8392 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
8393 
8394 		History:
8395 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
8396 
8397 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
8398 	+/
8399 	this(int width = 500, int height = 500, string title = null) {
8400 		if(title is null) {
8401 			import core.runtime;
8402 			if(Runtime.args.length)
8403 				title = Runtime.args[0];
8404 		}
8405 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus);
8406 
8407 		static if(UsingSimpledisplayX11) {
8408 		///+
8409 		// for input proxy
8410 		auto display = XDisplayConnection.get;
8411 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
8412 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
8413 		XMapWindow(display, inputProxy);
8414 		// writefln("input proxy: 0x%0x", inputProxy);
8415 		this.inputProxy = new SimpleWindow(inputProxy);
8416 
8417 		XEvent lastEvent;
8418 		this.inputProxy.handleNativeEvent = (XEvent ev) {
8419 			lastEvent = ev;
8420 			return 1;
8421 		};
8422 		this.inputProxy.setEventHandlers(
8423 			(MouseEvent e) {
8424 				dispatchMouseEvent(e);
8425 			},
8426 			(KeyEvent e) {
8427 				//writefln("%x   %s", cast(uint) e.key, e.key);
8428 				if(dispatchKeyEvent(e)) {
8429 					// FIXME: i should trap error
8430 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
8431 						auto thing = nw.focusableWindow();
8432 						if(thing && thing.window) {
8433 							lastEvent.xkey.window = thing.window;
8434 							// writeln("sending event ", lastEvent.xkey);
8435 							trapXErrors( {
8436 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
8437 							});
8438 						}
8439 					}
8440 				}
8441 			},
8442 			(dchar e) {
8443 				if(e == 13) e = 10; // hack?
8444 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8445 				dispatchCharEvent(e);
8446 			},
8447 		);
8448 
8449 		this.inputProxy.populateXic();
8450 		// done
8451 		//+/
8452 		}
8453 
8454 
8455 
8456 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
8457 
8458 		this(win);
8459 	}
8460 
8461 	SimpleWindow inputProxy;
8462 
8463 	private SimpleWindow setRequestedInputFocus() {
8464 		return inputProxy;
8465 	}
8466 
8467 	/// ditto
8468 	this(string title, int width = 500, int height = 500) {
8469 		this(width, height, title);
8470 	}
8471 
8472 	///
8473 	@property string title() { return parentWindow.win.title; }
8474 	///
8475 	@property void title(string title) { parentWindow.win.title = title; }
8476 
8477 	///
8478 	@scriptable
8479 	void close() {
8480 		win.close();
8481 		// I synchronize here upon window closing to ensure all child windows
8482 		// get updated too before the event loop. This avoids some random X errors.
8483 		static if(UsingSimpledisplayX11) {
8484 			runInGuiThread( {
8485 				XSync(XDisplayConnection.get, false);
8486 			});
8487 		}
8488 	}
8489 
8490 	bool dispatchKeyEvent(KeyEvent ev) {
8491 		auto wid = focusedWidget;
8492 		if(wid is null)
8493 			wid = this;
8494 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
8495 		event.originalKeyEvent = ev;
8496 		event.key = ev.key;
8497 		event.state = ev.modifierState;
8498 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8499 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8500 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8501 		event.dispatch();
8502 
8503 		return !event.propagationStopped;
8504 	}
8505 
8506 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
8507 	bool dispatchCharEvent(dchar ch) {
8508 		if(focusedWidget) {
8509 			auto event = new CharEvent(focusedWidget, ch);
8510 			event.dispatch();
8511 			return !event.propagationStopped;
8512 		}
8513 		return true;
8514 	}
8515 
8516 	Widget mouseLastOver;
8517 	Widget mouseLastDownOn;
8518 	bool lastWasDoubleClick;
8519 	bool dispatchMouseEvent(MouseEvent ev) {
8520 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
8521 		auto ele = eleR.widget;
8522 
8523 		auto captureEle = ele;
8524 
8525 		if(mouseCapturedBy !is null) {
8526 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
8527 				captureEle = mouseCapturedBy;
8528 		}
8529 
8530 		// a hack to get it relative to the widget.
8531 		eleR.x = ev.x;
8532 		eleR.y = ev.y;
8533 		auto pain = captureEle;
8534 		while(pain) {
8535 			eleR.x -= pain.x;
8536 			eleR.y -= pain.y;
8537 			pain.addScrollPosition(eleR.x, eleR.y);
8538 			pain = pain.parent;
8539 		}
8540 
8541 		void populateMouseEventBase(MouseEventBase event) {
8542 			event.button = ev.button;
8543 			event.buttonLinear = ev.buttonLinear;
8544 			event.state = ev.modifierState;
8545 			event.clientX = eleR.x;
8546 			event.clientY = eleR.y;
8547 
8548 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8549 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8550 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8551 		}
8552 
8553 		if(ev.type == MouseEventType.buttonPressed) {
8554 			{
8555 				auto event = new MouseDownEvent(captureEle);
8556 				populateMouseEventBase(event);
8557 				event.dispatch();
8558 			}
8559 
8560 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
8561 				auto event = new DoubleClickEvent(captureEle);
8562 				populateMouseEventBase(event);
8563 				event.dispatch();
8564 				lastWasDoubleClick = ev.doubleClick;
8565 			} else {
8566 				lastWasDoubleClick = false;
8567 			}
8568 
8569 			mouseLastDownOn = ele;
8570 		} else if(ev.type == MouseEventType.buttonReleased) {
8571 			{
8572 				auto event = new MouseUpEvent(captureEle);
8573 				populateMouseEventBase(event);
8574 				event.dispatch();
8575 			}
8576 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
8577 				auto event = new ClickEvent(captureEle);
8578 				populateMouseEventBase(event);
8579 				event.dispatch();
8580 			}
8581 		} else if(ev.type == MouseEventType.motion) {
8582 			// motion
8583 			{
8584 				auto event = new MouseMoveEvent(captureEle);
8585 				populateMouseEventBase(event); // fills in button which is meaningless but meh
8586 				event.dispatch();
8587 			}
8588 
8589 			if(mouseLastOver !is ele) {
8590 				if(ele !is null) {
8591 					if(!isAParentOf(ele, mouseLastOver)) {
8592 						ele.setDynamicState(DynamicState.hover, true);
8593 						auto event = new MouseEnterEvent(ele);
8594 						event.relatedTarget = mouseLastOver;
8595 						event.sendDirectly();
8596 
8597 						ele.useStyleProperties((scope Widget.Style s) {
8598 							ele.parentWindow.win.cursor = s.cursor;
8599 						});
8600 					}
8601 				}
8602 
8603 				if(mouseLastOver !is null) {
8604 					if(!isAParentOf(mouseLastOver, ele)) {
8605 						mouseLastOver.setDynamicState(DynamicState.hover, false);
8606 						auto event = new MouseLeaveEvent(mouseLastOver);
8607 						event.relatedTarget = ele;
8608 						event.sendDirectly();
8609 					}
8610 				}
8611 
8612 				if(ele !is null) {
8613 					auto event = new MouseOverEvent(ele);
8614 					event.relatedTarget = mouseLastOver;
8615 					event.dispatch();
8616 				}
8617 
8618 				if(mouseLastOver !is null) {
8619 					auto event = new MouseOutEvent(mouseLastOver);
8620 					event.relatedTarget = ele;
8621 					event.dispatch();
8622 				}
8623 
8624 				mouseLastOver = ele;
8625 			}
8626 		}
8627 
8628 		return true; // FIXME: the event default prevented?
8629 	}
8630 
8631 	/++
8632 		Shows the window and runs the application event loop.
8633 
8634 		Blocks until this window is closed.
8635 
8636 		Bugs:
8637 
8638 		$(PITFALL
8639 			You should always have one event loop live for your application.
8640 			If you make two windows in sequence, the second call to loop (or
8641 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
8642 			might fail:
8643 
8644 			---
8645 			// don't do this!
8646 			auto window = new Window();
8647 			window.loop();
8648 
8649 			// or new Window or new MainWindow, all the same
8650 			auto window2 = new SimpleWindow();
8651 			window2.eventLoop(0); // problematic! might crash
8652 			---
8653 
8654 			simpledisplay's current implementation assumes that final cleanup is
8655 			done when the event loop refcount reaches zero. So after the first
8656 			eventLoop returns, when there isn't already another one active, it assumes
8657 			the program will exit soon and cleans up.
8658 
8659 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
8660 			it eventually, but in the mean time, there's an easy solution:
8661 
8662 			---
8663 			// do this
8664 			EventLoop mainEventLoop = EventLoop.get; // just add this line
8665 
8666 			auto window = new Window();
8667 			window.loop();
8668 
8669 			// or any other type of Window etc.
8670 			auto window2 = new Window();
8671 			window2.loop(); // perfectly fine since mainEventLoop still alive
8672 			---
8673 
8674 			By adding a top-level reference to the event loop, it ensures the final cleanup
8675 			is not performed until it goes out of scope too, letting the individual window loops
8676 			work without trouble despite the bug.
8677 		)
8678 
8679 		History:
8680 			The [BlockingMode] parameter was added on December 8, 2021.
8681 			The default behavior is to block until the application quits
8682 			(so all windows have been closed), unless another minigui or
8683 			simpledisplay event loop is already running, in which case it
8684 			will block until this window closes specifically.
8685 	+/
8686 	@scriptable
8687 	void loop(BlockingMode bm = BlockingMode.automatic) {
8688 		if(win.closed)
8689 			return; // otherwise show will throw
8690 		show();
8691 		win.eventLoopWithBlockingMode(bm, 0);
8692 	}
8693 
8694 	private bool firstShow = true;
8695 
8696 	@scriptable
8697 	override void show() {
8698 		bool rd = false;
8699 		if(firstShow) {
8700 			firstShow = false;
8701 			queueRecomputeChildLayout();
8702 			auto f = getFirstFocusable(this); // FIXME: autofocus?
8703 			if(f)
8704 				f.focus();
8705 			redraw();
8706 		}
8707 		win.show();
8708 		super.show();
8709 	}
8710 	@scriptable
8711 	override void hide() {
8712 		win.hide();
8713 		super.hide();
8714 	}
8715 
8716 	static Widget getFirstFocusable(Widget start) {
8717 		if(start is null)
8718 			return null;
8719 
8720 		foreach(widget; &start.focusableWidgets) {
8721 			return widget;
8722 		}
8723 
8724 		return null;
8725 	}
8726 
8727 	static Widget getLastFocusable(Widget start) {
8728 		if(start is null)
8729 			return null;
8730 
8731 		Widget last;
8732 		foreach(widget; &start.focusableWidgets) {
8733 			last = widget;
8734 		}
8735 
8736 		return last;
8737 	}
8738 
8739 
8740 	mixin Emits!ClosingEvent;
8741 	mixin Emits!ClosedEvent;
8742 }
8743 
8744 /++
8745 	History:
8746 		Added January 12, 2022
8747 +/
8748 class DpiChangedEvent : Event {
8749 	enum EventString = "dpichanged";
8750 
8751 	this(Widget target) {
8752 		super(EventString, target);
8753 	}
8754 }
8755 
8756 debug private class DevToolWindow : Window {
8757 	Window p;
8758 
8759 	TextEdit parentList;
8760 	TextEdit logWindow;
8761 	TextLabel clickX, clickY;
8762 
8763 	this(Window p) {
8764 		this.p = p;
8765 		super(400, 300, "Developer Toolbox");
8766 
8767 		logWindow = new TextEdit(this);
8768 		parentList = new TextEdit(this);
8769 
8770 		auto hl = new HorizontalLayout(this);
8771 		clickX = new TextLabel("", TextAlignment.Right, hl);
8772 		clickY = new TextLabel("", TextAlignment.Right, hl);
8773 
8774 		parentListeners ~= p.addEventListener("*", (Event ev) {
8775 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
8776 		});
8777 
8778 		parentListeners ~= p.addEventListener((ClickEvent ev) {
8779 			auto s = ev.srcElement;
8780 
8781 			string list;
8782 
8783 			void addInfo(Widget s) {
8784 				list ~= s.toString();
8785 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
8786 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
8787 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
8788 				list ~= "\n\theight: " ~ toInternal!string(s.height);
8789 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
8790 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
8791 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
8792 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
8793 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
8794 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
8795 			}
8796 
8797 			addInfo(s);
8798 
8799 			s = s.parent;
8800 			while(s) {
8801 				list ~= "\n";
8802 				addInfo(s);
8803 				s = s.parent;
8804 			}
8805 			parentList.content = list;
8806 
8807 			clickX.label = toInternal!string(ev.clientX);
8808 			clickY.label = toInternal!string(ev.clientY);
8809 		});
8810 	}
8811 
8812 	EventListener[] parentListeners;
8813 
8814 	override void close() {
8815 		assert(p !is null);
8816 		foreach(p; parentListeners)
8817 			p.disconnect();
8818 		parentListeners = null;
8819 		p.devTools = null;
8820 		p = null;
8821 		super.close();
8822 	}
8823 
8824 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
8825 		if(ev.key == Key.F12) {
8826 			this.close();
8827 			if(p)
8828 				p.devTools = null;
8829 		} else {
8830 			super.defaultEventHandler_keydown(ev);
8831 		}
8832 	}
8833 
8834 	void log(T...)(T t) {
8835 		string str;
8836 		import std.conv;
8837 		foreach(i; t)
8838 			str ~= to!string(i);
8839 		str ~= "\n";
8840 		logWindow.addText(str);
8841 
8842 		//version(custom_widgets)
8843 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
8844 	}
8845 }
8846 
8847 /++
8848 	A dialog is a transient window that intends to get information from
8849 	the user before being dismissed.
8850 +/
8851 abstract class Dialog : Window {
8852 	///
8853 	this(int width, int height, string title = null) {
8854 		super(width, height, title);
8855 	}
8856 
8857 	///
8858 	abstract void OK();
8859 
8860 	///
8861 	void Cancel() {
8862 		this.close();
8863 	}
8864 }
8865 
8866 /++
8867 	A custom widget similar to the HTML5 <details> tag.
8868 +/
8869 version(none)
8870 class DetailsView : Widget {
8871 
8872 }
8873 
8874 // FIXME: maybe i should expose the other list views Windows offers too
8875 
8876 /++
8877 	A TableView is a widget made to display a table of data strings.
8878 
8879 
8880 	Future_Directions:
8881 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
8882 
8883 		I will add a selection changed event at some point, as well as item clicked events.
8884 	History:
8885 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
8886 	See_Also:
8887 		[ListWidget] which displays a list of strings without additional columns.
8888 +/
8889 class TableView : Widget {
8890 	/++
8891 
8892 	+/
8893 	this(Widget parent) {
8894 		super(parent);
8895 
8896 		version(win32_widgets) {
8897 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
8898 		} else version(custom_widgets) {
8899 			auto smw = new ScrollMessageWidget(this);
8900 			smw.addDefaultKeyboardListeners();
8901 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
8902 			tvwi = new TableViewWidgetInner(this, smw);
8903 		}
8904 	}
8905 
8906 	// FIXME: auto-size columns on double click of header thing like in Windows
8907 	// it need only make the currently displayed things fit well.
8908 
8909 
8910 	private ColumnInfo[] columns;
8911 	private int itemCount;
8912 
8913 	version(custom_widgets) private {
8914 		TableViewWidgetInner tvwi;
8915 	}
8916 
8917 	/// Passed to [setColumnInfo]
8918 	static struct ColumnInfo {
8919 		const(char)[] name; /// the name displayed in the header
8920 		/++
8921 			The default width, in pixels. As a special case, you can set this to -1
8922 			if you want the system to try to automatically size the width to fit visible
8923 			content. If it can't, it will try to pick a sensible default size.
8924 
8925 			Any other negative value is not allowed and may lead to unpredictable results.
8926 
8927 			History:
8928 				The -1 behavior was specified on December 3, 2021. It actually worked before
8929 				anyway on Win32 but now it is a formal feature with partial Linux support.
8930 
8931 			Bugs:
8932 				It doesn't actually attempt to calculate a best-fit width on Linux as of
8933 				December 3, 2021. I do plan to fix this in the future, but Windows is the
8934 				priority right now. At least it doesn't break things when you use it now.
8935 		+/
8936 		int width;
8937 
8938 		/++
8939 			Alignment of the text in the cell. Applies to the header as well as all data in this
8940 			column.
8941 
8942 			Bugs:
8943 				On Windows, the first column ignores this member and is always left aligned.
8944 				You can work around this by inserting a dummy first column with width = 0
8945 				then putting your actual data in the second column, which does respect the
8946 				alignment.
8947 
8948 				This is a quirk of the operating system's implementation going back a very
8949 				long time and is unlikely to ever be fixed.
8950 		+/
8951 		TextAlignment alignment;
8952 
8953 		/++
8954 			After all the pixel widths have been assigned, any left over
8955 			space is divided up among all columns and distributed to according
8956 			to the widthPercent field.
8957 
8958 
8959 			For example, if you have two fields, both with width 50 and one with
8960 			widthPercent of 25 and the other with widthPercent of 75, and the
8961 			container is 200 pixels wide, first both get their width of 50.
8962 			then the 100 remaining pixels are split up, so the one gets a total
8963 			of 75 pixels and the other gets a total of 125.
8964 
8965 			This is automatically applied as the window is resized.
8966 
8967 			If there is not enough space - that is, when a horizontal scrollbar
8968 			needs to appear - there are 0 pixels divided up, and thus everyone
8969 			gets 0. This can cause a column to shrink out of proportion when
8970 			passing the scroll threshold.
8971 
8972 			It is important to still set a fixed width (that is, to populate the
8973 			`width` field) even if you use the percents because that will be the
8974 			default minimum in the event of a scroll bar appearing.
8975 
8976 			The percents total in the column can never exceed 100 or be less than 0.
8977 			Doing this will trigger an assert error.
8978 
8979 			Implementation note:
8980 
8981 			Please note that percentages are only recalculated 1) upon original
8982 			construction and 2) upon resizing the control. If the user adjusts the
8983 			width of a column, the percentage items will not be updated.
8984 
8985 			On the other hand, if the user adjusts the width of a percentage column
8986 			then resizes the window, it is recalculated, meaning their hand adjustment
8987 			is discarded. This specific behavior may change in the future as it is
8988 			arguably a bug, but I'm not certain yet.
8989 
8990 			History:
8991 				Added November 10, 2021 (dub v10.4)
8992 		+/
8993 		int widthPercent;
8994 
8995 
8996 		private int calculatedWidth;
8997 	}
8998 	/++
8999 		Sets the number of columns along with information about the headers.
9000 
9001 		Please note: on Windows, the first column ignores your alignment preference
9002 		and is always left aligned.
9003 	+/
9004 	void setColumnInfo(ColumnInfo[] columns...) {
9005 
9006 		foreach(ref c; columns) {
9007 			c.name = c.name.idup;
9008 		}
9009 		this.columns = columns.dup;
9010 
9011 		updateCalculatedWidth(false);
9012 
9013 		version(custom_widgets) {
9014 			tvwi.header.updateHeaders();
9015 			tvwi.updateScrolls();
9016 		} else version(win32_widgets)
9017 		foreach(i, column; this.columns) {
9018 			LVCOLUMN lvColumn;
9019 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
9020 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
9021 
9022 			auto bfr = WCharzBuffer(column.name);
9023 			lvColumn.pszText = bfr.ptr;
9024 
9025 			if(column.alignment & TextAlignment.Center)
9026 				lvColumn.fmt = LVCFMT_CENTER;
9027 			else if(column.alignment & TextAlignment.Right)
9028 				lvColumn.fmt = LVCFMT_RIGHT;
9029 			else
9030 				lvColumn.fmt = LVCFMT_LEFT;
9031 
9032 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
9033 				throw new WindowsApiException("Insert Column Fail", GetLastError());
9034 		}
9035 	}
9036 
9037 	private int getActualSetSize(size_t i, bool askWindows) {
9038 		version(win32_widgets)
9039 			if(askWindows)
9040 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
9041 		auto w = columns[i].width;
9042 		if(w == -1)
9043 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9044 		return w;
9045 	}
9046 
9047 	private void updateCalculatedWidth(bool informWindows) {
9048 		int padding;
9049 		version(win32_widgets)
9050 			padding = 4;
9051 		int remaining = this.width;
9052 		foreach(i, column; columns)
9053 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9054 		remaining -= padding;
9055 		if(remaining < 0)
9056 			remaining = 0;
9057 
9058 		int percentTotal;
9059 		foreach(i, ref column; columns) {
9060 			percentTotal += column.widthPercent;
9061 
9062 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9063 
9064 			column.calculatedWidth = c;
9065 
9066 			version(win32_widgets)
9067 			if(informWindows)
9068 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9069 		}
9070 
9071 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9072 		assert(percentTotal <= 100, "The total percents in your column definitions exceeded 100. They must add up to no more than 100 (can be less though).");
9073 
9074 
9075 	}
9076 
9077 	override void registerMovement() {
9078 		super.registerMovement();
9079 
9080 		updateCalculatedWidth(true);
9081 	}
9082 
9083 	/++
9084 		Tells the view how many items are in it. It uses this to set the scroll bar, but the items are not added per se; it calls [getData] as-needed.
9085 	+/
9086 	void setItemCount(int count) {
9087 		this.itemCount = count;
9088 		version(custom_widgets) {
9089 			tvwi.updateScrolls();
9090 			redraw();
9091 		} else version(win32_widgets) {
9092 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9093 		}
9094 	}
9095 
9096 	/++
9097 		Clears all items;
9098 	+/
9099 	void clear() {
9100 		this.itemCount = 0;
9101 		this.columns = null;
9102 		version(custom_widgets) {
9103 			tvwi.header.updateHeaders();
9104 			tvwi.updateScrolls();
9105 			redraw();
9106 		} else version(win32_widgets) {
9107 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9108 		}
9109 	}
9110 
9111 	/+
9112 	version(win32_widgets)
9113 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9114 		auto itemId = dis.itemID;
9115 		auto hdc = dis.hDC;
9116 		auto rect = dis.rcItem;
9117 		switch(dis.itemAction) {
9118 			case ODA_DRAWENTIRE:
9119 
9120 				// FIXME: do other items
9121 				// FIXME: do the focus rectangle i guess
9122 				// FIXME: alignment
9123 				// FIXME: column width
9124 				// FIXME: padding left
9125 				// FIXME: check dpi scaling
9126 				// FIXME: don't owner draw unless it is necessary.
9127 
9128 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9129 				RECT itemRect;
9130 				itemRect.top = 1; // subitem idx, 1-based
9131 				itemRect.left = LVIR_BOUNDS;
9132 
9133 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9134 				itemRect.left += padding;
9135 
9136 				getData(itemId, 0, (in char[] data) {
9137 					auto wdata = WCharzBuffer(data);
9138 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9139 
9140 				});
9141 			goto case;
9142 			case ODA_FOCUS:
9143 				if(dis.itemState & ODS_FOCUS)
9144 					DrawFocusRect(hdc, &rect);
9145 			break;
9146 			case ODA_SELECT:
9147 				// itemState & ODS_SELECTED
9148 			break;
9149 			default:
9150 		}
9151 		return 1;
9152 	}
9153 	+/
9154 
9155 	version(win32_widgets) {
9156 		CellStyle last;
9157 		COLORREF defaultColor;
9158 		COLORREF defaultBackground;
9159 	}
9160 
9161 	version(win32_widgets)
9162 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9163 		switch(code) {
9164 			case NM_CUSTOMDRAW:
9165 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9166 				switch(s.nmcd.dwDrawStage) {
9167 					case CDDS_PREPAINT:
9168 						if(getCellStyle is null)
9169 							return 0;
9170 
9171 						mustReturn = true;
9172 						return CDRF_NOTIFYITEMDRAW;
9173 					case CDDS_ITEMPREPAINT:
9174 						mustReturn = true;
9175 						return CDRF_NOTIFYSUBITEMDRAW;
9176 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9177 						mustReturn = true;
9178 
9179 						if(getCellStyle is null) // this SHOULD never happen...
9180 							return 0;
9181 
9182 						if(s.iSubItem == 0) {
9183 							// Windows resets it per row so we'll use item 0 as a chance
9184 							// to capture these for later
9185 							defaultColor = s.clrText;
9186 							defaultBackground = s.clrTextBk;
9187 						}
9188 
9189 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9190 						// if no special style and no reset needed...
9191 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9192 							return 0; // allow default processing to continue
9193 
9194 						last = style;
9195 
9196 						// might still need to reset or use the preference.
9197 
9198 						if(style.flags & CellStyle.Flags.textColorSet)
9199 							s.clrText = style.textColor.asWindowsColorRef;
9200 						else
9201 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
9202 						if(style.flags & CellStyle.Flags.backgroundColorSet)
9203 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
9204 						else
9205 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
9206 
9207 						return CDRF_NEWFONT;
9208 					default:
9209 						return 0;
9210 
9211 				}
9212 			case NM_RETURN: // no need since i subclass keydown
9213 			break;
9214 			case LVN_COLUMNCLICK:
9215 				auto info = cast(LPNMLISTVIEW) hdr;
9216 				this.emit!HeaderClickedEvent(info.iSubItem);
9217 			break;
9218 			case NM_CLICK:
9219 			case NM_DBLCLK:
9220 			case NM_RCLICK:
9221 			case NM_RDBLCLK:
9222 				// the item/subitem is set here and that can be a useful notification
9223 				// even beyond the normal click notification
9224 			break;
9225 			case LVN_GETDISPINFO:
9226 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
9227 				if(info.item.mask & LVIF_TEXT) {
9228 					if(getData) {
9229 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
9230 							auto bfr = WCharzBuffer(dataReceived);
9231 							auto len = info.item.cchTextMax;
9232 							if(bfr.length < len)
9233 								len = cast(typeof(len)) bfr.length;
9234 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
9235 							info.item.pszText[len] = 0;
9236 						});
9237 					} else {
9238 						info.item.pszText[0] = 0;
9239 					}
9240 					//info.item.iItem
9241 					//if(info.item.iSubItem)
9242 				}
9243 			break;
9244 			default:
9245 		}
9246 		return 0;
9247 	}
9248 
9249 	override bool encapsulatedChildren() {
9250 		return true;
9251 	}
9252 
9253 	/++
9254 		Informs the control that content has changed.
9255 
9256 		History:
9257 			Added November 10, 2021 (dub v10.4)
9258 	+/
9259 	void update() {
9260 		version(custom_widgets)
9261 			redraw();
9262 		else {
9263 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
9264 			UpdateWindow(hwnd);
9265 		}
9266 
9267 
9268 	}
9269 
9270 	/++
9271 		Called by the system to request the text content of an individual cell. You
9272 		should pass the text into the provided `sink` delegate. This function will be
9273 		called for each visible cell as-needed when drawing.
9274 	+/
9275 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
9276 
9277 	/++
9278 		Available per-cell style customization options. Use one of the constructors
9279 		provided to set the values conveniently, or default construct it and set individual
9280 		values yourself. Just remember to set the `flags` so your values are actually used.
9281 		If the flag isn't set, the field is ignored and the system default is used instead.
9282 
9283 		This is returned by the [getCellStyle] delegate.
9284 
9285 		Examples:
9286 			---
9287 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
9288 			auto table = new TableView(window);
9289 			// snip: you would set up columns here
9290 
9291 			// this is how you provide data to the table view class
9292 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
9293 				import std.conv;
9294 				sink(to!string(my_data[row][column]));
9295 			};
9296 
9297 			// and this is how you customize the colors
9298 			table.getCellStyle = delegate(int row, int column) {
9299 				return (my_data[row][column] < 0) ?
9300 					TableView.CellStyle(Color.red); // make negative numbers red
9301 					: TableView.CellStyle.init; // leave the rest alone
9302 			};
9303 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
9304 			---
9305 
9306 		History:
9307 			Added November 27, 2021 (dub v10.4)
9308 	+/
9309 	struct CellStyle {
9310 		/// Sets just a custom text color, leaving the background as the default. Use caution with certain colors as it may have illeglible contrast on the (unknown to you) background color.
9311 		this(Color textColor) {
9312 			this.textColor = textColor;
9313 			this.flags |= Flags.textColorSet;
9314 		}
9315 		/// Sets a custom text and background color.
9316 		this(Color textColor, Color backgroundColor) {
9317 			this.textColor = textColor;
9318 			this.backgroundColor = backgroundColor;
9319 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
9320 		}
9321 
9322 		Color textColor;
9323 		Color backgroundColor;
9324 		int flags; /// bitmask of [Flags]
9325 		/// available options to combine into [flags]
9326 		enum Flags {
9327 			textColorSet = 1 << 0,
9328 			backgroundColorSet = 1 << 1,
9329 		}
9330 	}
9331 	/++
9332 		Companion delegate to [getData] that allows you to custom style each
9333 		cell of the table.
9334 
9335 		Returns:
9336 			A [CellStyle] structure that describes the desired style for the
9337 			given cell. `return CellStyle.init` if you want the default style.
9338 
9339 		History:
9340 			Added November 27, 2021 (dub v10.4)
9341 	+/
9342 	CellStyle delegate(int row, int column) getCellStyle;
9343 
9344 	// i want to be able to do things like draw little colored things to show red for negative numbers
9345 	// or background color indicators or even in-cell charts
9346 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
9347 
9348 	/++
9349 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
9350 	+/
9351 	mixin Emits!HeaderClickedEvent;
9352 }
9353 
9354 /++
9355 	This is emitted by the [TableView] when a user clicks on a column header.
9356 
9357 	Its member `columnIndex` has the zero-based index of the column that was clicked.
9358 
9359 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
9360 
9361 	History:
9362 		Added November 27, 2021 (dub v10.4)
9363 +/
9364 class HeaderClickedEvent : Event {
9365 	enum EventString = "HeaderClicked";
9366 	this(Widget target, int columnIndex) {
9367 		this.columnIndex = columnIndex;
9368 		super(EventString, target);
9369 	}
9370 
9371 	/// The index of the column
9372 	int columnIndex;
9373 
9374 	///
9375 	override @property int intValue() {
9376 		return columnIndex;
9377 	}
9378 }
9379 
9380 version(custom_widgets)
9381 private class TableViewWidgetInner : Widget {
9382 
9383 // wrap this thing in a ScrollMessageWidget
9384 
9385 	TableView tvw;
9386 	ScrollMessageWidget smw;
9387 	HeaderWidget header;
9388 
9389 	this(TableView tvw, ScrollMessageWidget smw) {
9390 		this.tvw = tvw;
9391 		this.smw = smw;
9392 		super(smw);
9393 
9394 		this.tabStop = true;
9395 
9396 		header = new HeaderWidget(this, smw.getHeader());
9397 
9398 		smw.addEventListener("scroll", () {
9399 			this.redraw();
9400 			header.redraw();
9401 		});
9402 
9403 
9404 		// I need headers outside the scroll area but rendered on the same line as the up arrow
9405 		// FIXME: add a fixed header to the SMW
9406 	}
9407 
9408 	enum padding = 3;
9409 
9410 	void updateScrolls() {
9411 		int w;
9412 		foreach(idx, column; tvw.columns) {
9413 			if(column.width == 0) continue;
9414 			w += tvw.getActualSetSize(idx, false);// + padding;
9415 		}
9416 		smw.setTotalArea(w, tvw.itemCount);
9417 		columnsWidth = w;
9418 	}
9419 
9420 	private int columnsWidth;
9421 
9422 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
9423 
9424 	override void registerMovement() {
9425 		super.registerMovement();
9426 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
9427 		smw.setViewableArea(this.width, this.height / lh);
9428 	}
9429 
9430 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
9431 		int x;
9432 		int y;
9433 
9434 		int row = smw.position.y;
9435 
9436 		foreach(lol; 0 .. this.height / lh) {
9437 			if(row >= tvw.itemCount)
9438 				break;
9439 			x = 0;
9440 			foreach(columnNumber, column; tvw.columns) {
9441 				auto x2 = x + column.calculatedWidth;
9442 				auto smwx = smw.position.x;
9443 
9444 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
9445 					auto startX = x;
9446 					auto endX = x + column.calculatedWidth;
9447 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
9448 						case TextAlignment.Left: startX += padding; break;
9449 						case TextAlignment.Center: startX += padding; endX -= padding; break;
9450 						case TextAlignment.Right: endX -= padding; break;
9451 						default: /* broken */ break;
9452 					}
9453 					if(column.width != 0) // no point drawing an invisible column
9454 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
9455 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh)));
9456 
9457 						void dotext(WidgetPainter painter) {
9458 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
9459 						}
9460 
9461 						if(tvw.getCellStyle !is null) {
9462 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
9463 
9464 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
9465 								auto tempPainter = painter;
9466 								tempPainter.fillColor = style.backgroundColor;
9467 								tempPainter.outlineColor = style.backgroundColor;
9468 
9469 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
9470 									Point(endX - smw.position.x, y + lh));
9471 							}
9472 							auto tempPainter = painter;
9473 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
9474 								tempPainter.outlineColor = style.textColor;
9475 
9476 							dotext(tempPainter);
9477 						} else {
9478 							dotext(painter);
9479 						}
9480 					});
9481 				}
9482 
9483 				x += column.calculatedWidth;
9484 			}
9485 			row++;
9486 			y += lh;
9487 		}
9488 		return bounds;
9489 	}
9490 
9491 	static class Style : Widget.Style {
9492 		override WidgetBackground background() {
9493 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
9494 		}
9495 	}
9496 	mixin OverrideStyle!Style;
9497 
9498 	private static class HeaderWidget : Widget {
9499 		/+
9500 			maybe i should do a splitter thing on top of the other widgets
9501 			so the splitter itself isn't really drawn but still replies to mouse events?
9502 		+/
9503 		this(TableViewWidgetInner tvw, Widget parent) {
9504 			super(parent);
9505 			this.tvw = tvw;
9506 
9507 			this.remainder = new Button("", this);
9508 
9509 			this.addEventListener((scope ClickEvent ev) {
9510 				int header = -1;
9511 				foreach(idx, child; this.children[1 .. $]) {
9512 					if(child is ev.target) {
9513 						header = cast(int) idx;
9514 						break;
9515 					}
9516 				}
9517 
9518 				if(header != -1) {
9519 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
9520 					hce.dispatch();
9521 				}
9522 
9523 			});
9524 		}
9525 
9526 		void updateHeaders() {
9527 			foreach(child; children[1 .. $])
9528 				child.removeWidget();
9529 
9530 			foreach(column; tvw.tvw.columns) {
9531 				// the cast is ok because I dup it above, just the type is never changed.
9532 				// all this is private so it should never get messed up.
9533 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
9534 			}
9535 		}
9536 
9537 		Button remainder;
9538 		TableViewWidgetInner tvw;
9539 
9540 		override void recomputeChildLayout() {
9541 			registerMovement();
9542 			int pos;
9543 			foreach(idx, child; children[1 .. $]) {
9544 				if(idx >= tvw.tvw.columns.length)
9545 					continue;
9546 				child.x = pos;
9547 				child.y = 0;
9548 				child.width = tvw.tvw.columns[idx].calculatedWidth;
9549 				child.height = scaleWithDpi(16);// this.height;
9550 				pos += child.width;
9551 
9552 				child.recomputeChildLayout();
9553 			}
9554 
9555 			if(remainder is null)
9556 				return;
9557 
9558 			remainder.x = pos;
9559 			remainder.y = 0;
9560 			if(pos < this.width)
9561 				remainder.width = this.width - pos;// + 4;
9562 			else
9563 				remainder.width = 0;
9564 			remainder.height = scaleWithDpi(16);
9565 
9566 			remainder.recomputeChildLayout();
9567 		}
9568 
9569 		// for the scrollable children mixin
9570 		Point scrollOrigin() {
9571 			return Point(tvw.smw.position.x, 0);
9572 		}
9573 		void paintFrameAndBackground(WidgetPainter painter) { }
9574 
9575 		mixin ScrollableChildren;
9576 	}
9577 }
9578 
9579 /+
9580 
9581 // given struct / array / number / string / etc, make it viewable and editable
9582 class DataViewerWidget : Widget {
9583 
9584 }
9585 +/
9586 
9587 /++
9588 	A line edit box with an associated label.
9589 
9590 	History:
9591 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
9592 
9593 		```
9594 		Old: ________
9595 
9596 		New:
9597 		____________
9598 		```
9599 
9600 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
9601 
9602 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
9603 		horizontal label but left aligned. You may also consider a [GridLayout].
9604 +/
9605 alias LabeledLineEdit = Labeled!LineEdit;
9606 
9607 private int widthThatWouldFitChildLabels(Widget w) {
9608 	if(w is null)
9609 		return 0;
9610 
9611 	int max;
9612 
9613 	if(auto label = cast(TextLabel) w) {
9614 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
9615 	} else {
9616 		foreach(child; w.children) {
9617 			max = mymax(max, widthThatWouldFitChildLabels(child));
9618 		}
9619 	}
9620 
9621 	return max;
9622 }
9623 
9624 /++
9625 	History:
9626 		Added May 19, 2021
9627 +/
9628 class Labeled(T) : Widget {
9629 	///
9630 	this(string label, Widget parent) {
9631 		super(parent);
9632 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
9633 	}
9634 
9635 	/++
9636 		History:
9637 			The alignment parameter was added May 17, 2021
9638 	+/
9639 	this(string label, TextAlignment alignment, Widget parent) {
9640 		super(parent);
9641 		initialize!HorizontalLayout(label, alignment, parent);
9642 	}
9643 
9644 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
9645 		tabStop = false;
9646 		horizontal = is(L == HorizontalLayout);
9647 		auto hl = new L(this);
9648 		if(horizontal) {
9649 			static class SpecialTextLabel : TextLabel {
9650 				Widget outerParent;
9651 
9652 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
9653 					this.outerParent = outerParent;
9654 					super(label, alignment, parent);
9655 				}
9656 
9657 				override int flexBasisWidth() {
9658 					return widthThatWouldFitChildLabels(outerParent);
9659 				}
9660 				/+
9661 				override int widthShrinkiness() { return 0; }
9662 				override int widthStretchiness() { return 1; }
9663 				+/
9664 
9665 				override int paddingRight() { return 6; }
9666 				override int paddingLeft() { return 9; }
9667 
9668 				override int paddingTop() { return 3; }
9669 			}
9670 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
9671 		} else
9672 			this.label = new TextLabel(label, alignment, hl);
9673 		this.lineEdit = new T(hl);
9674 
9675 		this.label.labelFor = this.lineEdit;
9676 	}
9677 
9678 	private bool horizontal;
9679 
9680 	TextLabel label; ///
9681 	T lineEdit; ///
9682 
9683 	override int flexBasisWidth() { return 250; }
9684 	override int widthShrinkiness() { return 1; }
9685 
9686 	override int minHeight() {
9687 		return this.children[0].minHeight;
9688 	}
9689 	override int maxHeight() { return minHeight(); }
9690 	override int marginTop() { return 4; }
9691 	override int marginBottom() { return 4; }
9692 
9693 	// FIXME: i should prolly call it value as well as content tbh
9694 
9695 	///
9696 	@property string content() {
9697 		return lineEdit.content;
9698 	}
9699 	///
9700 	@property void content(string c) {
9701 		return lineEdit.content(c);
9702 	}
9703 
9704 	///
9705 	void selectAll() {
9706 		lineEdit.selectAll();
9707 	}
9708 
9709 	override void focus() {
9710 		lineEdit.focus();
9711 	}
9712 }
9713 
9714 /++
9715 	A labeled password edit.
9716 
9717 	History:
9718 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
9719 
9720 		The default parameters for the constructors were also removed on May 19, 2021
9721 +/
9722 alias LabeledPasswordEdit = Labeled!PasswordEdit;
9723 
9724 private string toMenuLabel(string s) {
9725 	string n;
9726 	n.reserve(s.length);
9727 	foreach(c; s)
9728 		if(c == '_')
9729 			n ~= ' ';
9730 		else
9731 			n ~= c;
9732 	return n;
9733 }
9734 
9735 private void autoExceptionHandler(Exception e) {
9736 	messageBox(e.msg);
9737 }
9738 
9739 private void delegate() makeAutomaticHandler(alias fn, T)(T t) {
9740 	static if(is(T : void delegate())) {
9741 		return () {
9742 			try
9743 				t();
9744 			catch(Exception e)
9745 				autoExceptionHandler(e);
9746 		};
9747 	} else static if(is(typeof(fn) Params == __parameters)) {
9748 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
9749 			return () {
9750 				void onOK(string s) {
9751 					member = s;
9752 					try
9753 						t(Params[0](s));
9754 					catch(Exception e)
9755 						autoExceptionHandler(e);
9756 				}
9757 
9758 				if(
9759 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
9760 					|| type == FileDialogType.Save)
9761 				{
9762 					getSaveFileName(&onOK, member, filters, null);
9763 				} else
9764 					getOpenFileName(&onOK, member, filters, null);
9765 			};
9766 		} else {
9767 			struct S {
9768 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
9769 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
9770 				} else mixin(q{
9771 				static foreach(idx, ignore; Params) {
9772 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
9773 				}
9774 				});
9775 			}
9776 			return () {
9777 				dialog((S s) {
9778 					try {
9779 						static if(is(typeof(t) Ret == return)) {
9780 							static if(is(Ret == void)) {
9781 								t(s.tupleof);
9782 							} else {
9783 								auto ret = t(s.tupleof);
9784 								import std.conv;
9785 								messageBox(to!string(ret), "Returned Value");
9786 							}
9787 						}
9788 					} catch(Exception e)
9789 						autoExceptionHandler(e);
9790 				}, null, __traits(identifier, fn));
9791 			};
9792 		}
9793 	}
9794 }
9795 
9796 private template hasAnyRelevantAnnotations(a...) {
9797 	bool helper() {
9798 		bool any;
9799 		foreach(attr; a) {
9800 			static if(is(typeof(attr) == .menu))
9801 				any = true;
9802 			else static if(is(typeof(attr) == .toolbar))
9803 				any = true;
9804 			else static if(is(attr == .separator))
9805 				any = true;
9806 			else static if(is(typeof(attr) == .accelerator))
9807 				any = true;
9808 			else static if(is(typeof(attr) == .hotkey))
9809 				any = true;
9810 			else static if(is(typeof(attr) == .icon))
9811 				any = true;
9812 			else static if(is(typeof(attr) == .label))
9813 				any = true;
9814 			else static if(is(typeof(attr) == .tip))
9815 				any = true;
9816 		}
9817 		return any;
9818 	}
9819 
9820 	enum bool hasAnyRelevantAnnotations = helper();
9821 }
9822 
9823 /++
9824 	A `MainWindow` is a window that includes turnkey support for a menu bar, tool bar, and status bar automatically positioned around a client area where you put your widgets.
9825 +/
9826 class MainWindow : Window {
9827 	///
9828 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
9829 		super(initialWidth, initialHeight, title);
9830 
9831 		_clientArea = new ClientAreaWidget();
9832 		_clientArea.x = 0;
9833 		_clientArea.y = 0;
9834 		_clientArea.width = this.width;
9835 		_clientArea.height = this.height;
9836 		_clientArea.tabStop = false;
9837 
9838 		super.addChild(_clientArea);
9839 
9840 		statusBar = new StatusBar(this);
9841 	}
9842 
9843 	/++
9844 		Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though).
9845 
9846 	---
9847         struct Commands {
9848                 @menu("File") {
9849 			@toolbar("") // adds it to a generic toolbar
9850                         void New() {}
9851                         void Open() {}
9852                         void Save() {}
9853                         @separator
9854                         void Exit() @accelerator("Alt+F4") @hotkey('x') {
9855                                 window.close();
9856                         }
9857                 }
9858 
9859                 @menu("Edit") {
9860 			@icon(GenericIcons.Undo)
9861                         void Undo() {
9862                                 undo();
9863                         }
9864                         @separator
9865                         void Cut() {}
9866                         void Copy() {}
9867                         void Paste() {}
9868                 }
9869 
9870                 @menu("Help") {
9871                         void About() {}
9872                 }
9873         }
9874 
9875         Commands commands;
9876 
9877         window.setMenuAndToolbarFromAnnotatedCode(commands);
9878 	---
9879 
9880 	Note that you can call this function multiple times and it will add the items in order to the given items.
9881 
9882 	+/
9883 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
9884 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9885 	}
9886 	/// ditto
9887 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
9888 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9889 	}
9890 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
9891 		Action[] toolbarActions;
9892 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
9893 		Menu[string] mcs;
9894 
9895 		foreach(menu; menuBar.subMenus) {
9896 			mcs[menu.label] = menu;
9897 		}
9898 
9899 		foreach(memberName; __traits(derivedMembers, T)) {
9900 			static if(memberName != "this")
9901 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
9902 				.menu menu;
9903 				.toolbar toolbar;
9904 				bool separator;
9905 				.accelerator accelerator;
9906 				.hotkey hotkey;
9907 				.icon icon;
9908 				string label;
9909 				string tip;
9910 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
9911 					static if(is(typeof(attr) == .menu))
9912 						menu = attr;
9913 					else static if(is(typeof(attr) == .toolbar))
9914 						toolbar = attr;
9915 					else static if(is(attr == .separator))
9916 						separator = true;
9917 					else static if(is(typeof(attr) == .accelerator))
9918 						accelerator = attr;
9919 					else static if(is(typeof(attr) == .hotkey))
9920 						hotkey = attr;
9921 					else static if(is(typeof(attr) == .icon))
9922 						icon = attr;
9923 					else static if(is(typeof(attr) == .label))
9924 						label = attr.label;
9925 					else static if(is(typeof(attr) == .tip))
9926 						tip = attr.tip;
9927 				}
9928 
9929 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
9930 					ushort correctIcon = icon.id; // FIXME
9931 					if(label.length == 0)
9932 						label = memberName.toMenuLabel;
9933 
9934 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName));
9935 
9936 					auto action = new Action(label, correctIcon, handler);
9937 
9938 					if(accelerator.keyString.length) {
9939 						auto ke = KeyEvent.parse(accelerator.keyString);
9940 						action.accelerator = ke;
9941 						accelerators[ke.toStr] = handler;
9942 					}
9943 
9944 					if(toolbar !is .toolbar.init)
9945 						toolbarActions ~= action;
9946 					if(menu !is .menu.init) {
9947 						Menu mc;
9948 						if(menu.name in mcs) {
9949 							mc = mcs[menu.name];
9950 						} else {
9951 							mc = new Menu(menu.name, this);
9952 							menuBar.addItem(mc);
9953 							mcs[menu.name] = mc;
9954 						}
9955 
9956 						if(separator)
9957 							mc.addSeparator();
9958 						mc.addItem(new MenuItem(action));
9959 					}
9960 				}
9961 			}
9962 		}
9963 
9964 		this.menuBar = menuBar;
9965 
9966 		if(toolbarActions.length) {
9967 			auto tb = new ToolBar(toolbarActions, this);
9968 		}
9969 	}
9970 
9971 	void delegate()[string] accelerators;
9972 
9973 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9974 		auto str = event.originalKeyEvent.toStr;
9975 		if(auto acl = str in accelerators)
9976 			(*acl)();
9977 		super.defaultEventHandler_keydown(event);
9978 	}
9979 
9980 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
9981 		super.defaultEventHandler_mouseover(event);
9982 		if(this.statusBar !is null && event.target.statusTip.length)
9983 			this.statusBar.parts[0].content = event.target.statusTip;
9984 		else if(this.statusBar !is null && this.statusTip.length)
9985 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
9986 	}
9987 
9988 	override void addChild(Widget c, int position = int.max) {
9989 		if(auto tb = cast(ToolBar) c)
9990 			version(win32_widgets)
9991 				super.addChild(c, 0);
9992 			else version(custom_widgets)
9993 				super.addChild(c, menuBar ? 1 : 0);
9994 			else static assert(0);
9995 		else
9996 			clientArea.addChild(c, position);
9997 	}
9998 
9999 	ToolBar _toolBar;
10000 	///
10001 	ToolBar toolBar() { return _toolBar; }
10002 	///
10003 	ToolBar toolBar(ToolBar t) {
10004 		_toolBar = t;
10005 		foreach(child; this.children)
10006 			if(child is t)
10007 				return t;
10008 		version(win32_widgets)
10009 			super.addChild(t, 0);
10010 		else version(custom_widgets)
10011 			super.addChild(t, menuBar ? 1 : 0);
10012 		else static assert(0);
10013 		return t;
10014 	}
10015 
10016 	MenuBar _menu;
10017 	///
10018 	MenuBar menuBar() { return _menu; }
10019 	///
10020 	MenuBar menuBar(MenuBar m) {
10021 		if(m is _menu) {
10022 			version(custom_widgets)
10023 				queueRecomputeChildLayout();
10024 			return m;
10025 		}
10026 
10027 		if(_menu !is null) {
10028 			// make sure it is sanely removed
10029 			// FIXME
10030 		}
10031 
10032 		_menu = m;
10033 
10034 		version(win32_widgets) {
10035 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
10036 		} else version(custom_widgets) {
10037 			super.addChild(m, 0);
10038 
10039 		//	clientArea.y = menu.height;
10040 		//	clientArea.height = this.height - menu.height;
10041 
10042 			queueRecomputeChildLayout();
10043 		} else static assert(false);
10044 
10045 		return _menu;
10046 	}
10047 	private Widget _clientArea;
10048 	///
10049 	@property Widget clientArea() { return _clientArea; }
10050 	protected @property void clientArea(Widget wid) {
10051 		_clientArea = wid;
10052 	}
10053 
10054 	private StatusBar _statusBar;
10055 	/++
10056 		Returns the window's [StatusBar]. Be warned it may be `null`.
10057 	+/
10058 	@property StatusBar statusBar() { return _statusBar; }
10059 	/// ditto
10060 	@property void statusBar(StatusBar bar) {
10061 		if(_statusBar !is null)
10062 			_statusBar.removeWidget();
10063 		_statusBar = bar;
10064 		if(bar !is null)
10065 			super.addChild(_statusBar);
10066 	}
10067 }
10068 
10069 /+
10070 	This is really an implementation detail of [MainWindow]
10071 +/
10072 private class ClientAreaWidget : Widget {
10073 	this() {
10074 		this.tabStop = false;
10075 		super(null);
10076 		//sa = new ScrollableWidget(this);
10077 	}
10078 	/*
10079 	ScrollableWidget sa;
10080 	override void addChild(Widget w, int position) {
10081 		if(sa is null)
10082 			super.addChild(w, position);
10083 		else {
10084 			sa.addChild(w, position);
10085 			sa.setContentSize(this.minWidth + 1, this.minHeight);
10086 			writeln(sa.contentWidth, "x", sa.contentHeight);
10087 		}
10088 	}
10089 	*/
10090 }
10091 
10092 /**
10093 	Toolbars are lists of buttons (typically icons) that appear under the menu.
10094 	Each button ought to correspond to a menu item, represented by [Action] objects.
10095 */
10096 class ToolBar : Widget {
10097 	version(win32_widgets) {
10098 		private int idealHeight;
10099 		override int minHeight() { return idealHeight; }
10100 		override int maxHeight() { return idealHeight; }
10101 	} else version(custom_widgets) {
10102 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
10103 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
10104 	} else static assert(false);
10105 	override int heightStretchiness() { return 0; }
10106 
10107 	version(win32_widgets) {
10108 		HIMAGELIST imageListSmall;
10109 		HIMAGELIST imageListLarge;
10110 	}
10111 
10112 	this(Widget parent) {
10113 		this(null, parent);
10114 	}
10115 
10116 	version(win32_widgets)
10117 	void changeIconSize(bool useLarge) {
10118 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
10119 
10120 		/+
10121 		SIZE size;
10122 		import core.sys.windows.commctrl;
10123 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
10124 		idealHeight = size.cy + 4; // the plus 4 is a hack
10125 		+/
10126 
10127 		idealHeight = useLarge ? 34 : 26;
10128 
10129 		if(parent) {
10130 			parent.queueRecomputeChildLayout();
10131 			parent.redraw();
10132 		}
10133 
10134 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
10135 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
10136 	}
10137 
10138 	///
10139 	this(Action[] actions, Widget parent) {
10140 		super(parent);
10141 
10142 		tabStop = false;
10143 
10144 		version(win32_widgets) {
10145 			// so i like how the flat thing looks on windows, but not on wine
10146 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
10147 			// leave it commented
10148 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
10149 
10150 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
10151 
10152 			imageListSmall = ImageList_Create(
10153 				// width, height
10154 				16, 16,
10155 				ILC_COLOR16 | ILC_MASK,
10156 				16 /*numberOfButtons*/, 0);
10157 
10158 			imageListLarge = ImageList_Create(
10159 				// width, height
10160 				24, 24,
10161 				ILC_COLOR16 | ILC_MASK,
10162 				16 /*numberOfButtons*/, 0);
10163 
10164 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
10165 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
10166 
10167 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
10168 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
10169 
10170 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
10171 
10172 			TBBUTTON[] buttons;
10173 
10174 			// FIXME: I_IMAGENONE is if here is no icon
10175 			foreach(action; actions)
10176 				buttons ~= TBBUTTON(
10177 					MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
10178 					action.id,
10179 					TBSTATE_ENABLED, // state
10180 					0, // style
10181 					0, // reserved array, just zero it out
10182 					0, // dwData
10183 					cast(size_t) toWstringzInternal(action.label) // INT_PTR
10184 				);
10185 
10186 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
10187 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
10188 
10189 			/*
10190 			RECT rect;
10191 			GetWindowRect(hwnd, &rect);
10192 			idealHeight = rect.bottom - rect.top + 10; // the +10 is a hack since the size right now doesn't look right on a real Windows XP box
10193 			*/
10194 
10195 			dpiChanged(); // to load the things calling changeIconSize the first time
10196 
10197 			assert(idealHeight);
10198 		} else version(custom_widgets) {
10199 			foreach(action; actions)
10200 				new ToolButton(action, this);
10201 		} else static assert(false);
10202 	}
10203 
10204 	override void recomputeChildLayout() {
10205 		.recomputeChildLayout!"width"(this);
10206 	}
10207 
10208 
10209 	version(win32_widgets)
10210 	override protected void dpiChanged() {
10211 		auto sz = scaleWithDpi(16);
10212 		if(sz >= 20)
10213 			changeIconSize(true);
10214 		else
10215 			changeIconSize(false);
10216 	}
10217 }
10218 
10219 enum toolbarIconSize = 24;
10220 
10221 /// An implementation helper for [ToolBar]. Generally, you shouldn't create these yourself and instead just pass [Action]s to [ToolBar]'s constructor and let it create the buttons for you.
10222 class ToolButton : Button {
10223 	///
10224 	this(string label, Widget parent) {
10225 		super(label, parent);
10226 		tabStop = false;
10227 	}
10228 	///
10229 	this(Action action, Widget parent) {
10230 		super(action.label, parent);
10231 		tabStop = false;
10232 		this.action = action;
10233 	}
10234 
10235 	version(custom_widgets)
10236 	override void defaultEventHandler_click(ClickEvent event) {
10237 		foreach(handler; action.triggered)
10238 			handler();
10239 	}
10240 
10241 	Action action;
10242 
10243 	override int maxWidth() { return toolbarIconSize; }
10244 	override int minWidth() { return toolbarIconSize; }
10245 	override int maxHeight() { return toolbarIconSize; }
10246 	override int minHeight() { return toolbarIconSize; }
10247 
10248 	version(custom_widgets)
10249 	override void paint(WidgetPainter painter) {
10250 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
10251 		painter.outlineColor = Color.black;
10252 
10253 		// I want to get from 16 to 24. that's * 3 / 2
10254 		static assert(toolbarIconSize >= 16);
10255 		enum multiplier = toolbarIconSize / 8;
10256 		enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0);
10257 		switch(action.iconId) {
10258 			case GenericIcons.New:
10259 				painter.fillColor = Color.white;
10260 				painter.drawPolygon(
10261 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor,
10262 					Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor,
10263 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor
10264 				);
10265 			break;
10266 			case GenericIcons.Save:
10267 				painter.fillColor = Color.white;
10268 				painter.outlineColor = Color.black;
10269 				painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10270 
10271 				// the label
10272 				painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor);
10273 
10274 				// the slider
10275 				painter.fillColor = Color.black;
10276 				painter.outlineColor = Color.black;
10277 				painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor);
10278 
10279 				painter.fillColor = Color.white;
10280 				painter.outlineColor = Color.white;
10281 				// the disc window
10282 				painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor);
10283 			break;
10284 			case GenericIcons.Open:
10285 				painter.fillColor = Color.white;
10286 				painter.drawPolygon(
10287 					Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor,
10288 					Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor);
10289 				painter.drawPolygon(
10290 					Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor,
10291 					Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor,
10292 					Point(2, 6) * multiplier / divisor);
10293 				//painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor);
10294 			break;
10295 			case GenericIcons.Copy:
10296 				painter.fillColor = Color.white;
10297 				painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor);
10298 				painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor);
10299 			break;
10300 			case GenericIcons.Cut:
10301 				painter.fillColor = Color.transparent;
10302 				painter.outlineColor = getComputedStyle.foregroundColor();
10303 				painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor);
10304 				painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor);
10305 				painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor);
10306 				painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor);
10307 			break;
10308 			case GenericIcons.Paste:
10309 				painter.fillColor = Color.white;
10310 				painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor);
10311 				painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10312 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor);
10313 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor);
10314 				painter.fillColor = Color.black;
10315 				painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor);
10316 			break;
10317 			case GenericIcons.Help:
10318 				painter.outlineColor = getComputedStyle.foregroundColor();
10319 				painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10320 			break;
10321 			case GenericIcons.Undo:
10322 				painter.fillColor = Color.transparent;
10323 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10324 				painter.outlineColor = Color.black;
10325 				painter.fillColor = Color.black;
10326 				painter.drawPolygon(
10327 					Point(4, 4) * multiplier / divisor,
10328 					Point(8, 2) * multiplier / divisor,
10329 					Point(8, 6) * multiplier / divisor,
10330 					Point(4, 4) * multiplier / divisor,
10331 				);
10332 			break;
10333 			case GenericIcons.Redo:
10334 				painter.fillColor = Color.transparent;
10335 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10336 				painter.outlineColor = Color.black;
10337 				painter.fillColor = Color.black;
10338 				painter.drawPolygon(
10339 					Point(10, 4) * multiplier / divisor,
10340 					Point(6, 2) * multiplier / divisor,
10341 					Point(6, 6) * multiplier / divisor,
10342 					Point(10, 4) * multiplier / divisor,
10343 				);
10344 			break;
10345 			default:
10346 				painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10347 		}
10348 		return bounds;
10349 		});
10350 	}
10351 
10352 }
10353 
10354 
10355 /++
10356 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
10357 +/
10358 class MenuBar : Widget {
10359 	MenuItem[] items;
10360 	Menu[] subMenus;
10361 
10362 	version(win32_widgets) {
10363 		HMENU handle;
10364 		///
10365 		this(Widget parent = null) {
10366 			super(parent);
10367 
10368 			handle = CreateMenu();
10369 			tabStop = false;
10370 		}
10371 	} else version(custom_widgets) {
10372 		///
10373 		this(Widget parent = null) {
10374 			tabStop = false; // these are selected some other way
10375 			super(parent);
10376 		}
10377 
10378 		mixin Padding!q{2};
10379 	} else static assert(false);
10380 
10381 	version(custom_widgets)
10382 	override void paint(WidgetPainter painter) {
10383 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
10384 	}
10385 
10386 	///
10387 	MenuItem addItem(MenuItem item) {
10388 		this.addChild(item);
10389 		items ~= item;
10390 		version(win32_widgets) {
10391 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
10392 		}
10393 		return item;
10394 	}
10395 
10396 
10397 	///
10398 	Menu addItem(Menu item) {
10399 
10400 		subMenus ~= item;
10401 
10402 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
10403 
10404 		addChild(mbItem);
10405 		items ~= mbItem;
10406 
10407 		version(win32_widgets) {
10408 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
10409 		} else version(custom_widgets) {
10410 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
10411 				item.popup(mbItem);
10412 			};
10413 		} else static assert(false);
10414 
10415 		return item;
10416 	}
10417 
10418 	override void recomputeChildLayout() {
10419 		.recomputeChildLayout!"width"(this);
10420 	}
10421 
10422 	override int maxHeight() { return defaultLineHeight + 4; }
10423 	override int minHeight() { return defaultLineHeight + 4; }
10424 }
10425 
10426 
10427 /**
10428 	Status bars appear at the bottom of a MainWindow.
10429 	They are made out of Parts, with a width and content.
10430 
10431 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
10432 
10433 
10434 	sb.parts[0].content = "Status bar text!";
10435 */
10436 class StatusBar : Widget {
10437 	private Part[] partsArray;
10438 	///
10439 	struct Parts {
10440 		@disable this();
10441 		this(StatusBar owner) { this.owner = owner; }
10442 		//@disable this(this);
10443 		///
10444 		@property int length() { return cast(int) owner.partsArray.length; }
10445 		private StatusBar owner;
10446 		private this(StatusBar owner, Part[] parts) {
10447 			this.owner.partsArray = parts;
10448 			this.owner = owner;
10449 		}
10450 		///
10451 		Part opIndex(int p) {
10452 			if(owner.partsArray.length == 0)
10453 				this ~= new StatusBar.Part(0);
10454 			return owner.partsArray[p];
10455 		}
10456 
10457 		///
10458 		Part opOpAssign(string op : "~" )(Part p) {
10459 			assert(owner.partsArray.length < 255);
10460 			p.owner = this.owner;
10461 			p.idx = cast(int) owner.partsArray.length;
10462 			owner.partsArray ~= p;
10463 
10464 			owner.queueRecomputeChildLayout();
10465 
10466 			version(win32_widgets) {
10467 				int[256] pos;
10468 				int cpos;
10469 				foreach(idx, part; owner.partsArray) {
10470 					if(idx + 1 == owner.partsArray.length)
10471 						pos[idx] = -1;
10472 					else {
10473 						cpos += part.currentlyAssignedWidth;
10474 						pos[idx] = cpos;
10475 					}
10476 				}
10477 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
10478 			} else version(custom_widgets) {
10479 				owner.redraw();
10480 			} else static assert(false);
10481 
10482 			return p;
10483 		}
10484 	}
10485 
10486 	private Parts _parts;
10487 	///
10488 	final @property Parts parts() {
10489 		return _parts;
10490 	}
10491 
10492 	/++
10493 
10494 	+/
10495 	static class Part {
10496 		/++
10497 			History:
10498 				Added September 1, 2023 (dub v11.1)
10499 		+/
10500 		enum WidthUnits {
10501 			/++
10502 				Unscaled pixels as they appear on screen.
10503 
10504 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
10505 			+/
10506 			DeviceDependentPixels,
10507 			/++
10508 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
10509 			+/
10510 			DeviceIndependentPixels,
10511 			/++
10512 				An approximate character count in the currently selected font (at layout time) of the status bar. This will use the x-width (similar to css `ch`).
10513 			+/
10514 			ApproximateCharacters,
10515 			/++
10516 				These take a proportion of the remaining space in the window after all other parts have been assigned. The sum of all proportional parts is then divided by the current item to get the amount of space it uses.
10517 
10518 				If you pass 0, it will assume that this item takes an average of all remaining proportional space. This is there primarily to provide compatibility with code written against older versions of minigui.
10519 			+/
10520 			Proportional
10521 		}
10522 		private WidthUnits units;
10523 		private int width;
10524 		private StatusBar owner;
10525 
10526 		private int currentlyAssignedWidth;
10527 
10528 		/++
10529 			History:
10530 				Prior to September 1, 2023, this took a default value of 100 and was interpreted as pixels, unless the value was 0 and it was the last item in the list, in which case it would use the remaining space in the window.
10531 
10532 				It now allows you to provide your own value for [WidthUnits].
10533 
10534 				Additionally, the default value used to be an arbitrary value of 100. It is now 0, to take advantage of the automatic proportional calculator in the new version. If you want the old behavior, pass `100, StatusBar.Part.WidthUnits.DeviceIndependentPixels`.
10535 		+/
10536 		this(int w, WidthUnits units = WidthUnits.Proportional) {
10537 			this.units = units;
10538 			this.width = w;
10539 		}
10540 
10541 		/// ditto
10542 		this(int w = 0) {
10543 			if(w == 0)
10544 				this(w, WidthUnits.Proportional);
10545 			else
10546 				this(w, WidthUnits.DeviceDependentPixels);
10547 		}
10548 
10549 		private int idx;
10550 		private string _content;
10551 		///
10552 		@property string content() { return _content; }
10553 		///
10554 		@property void content(string s) {
10555 			version(win32_widgets) {
10556 				_content = s;
10557 				WCharzBuffer bfr = WCharzBuffer(s);
10558 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
10559 			} else version(custom_widgets) {
10560 				if(_content != s) {
10561 					_content = s;
10562 					owner.redraw();
10563 				}
10564 			} else static assert(false);
10565 		}
10566 	}
10567 	string simpleModeContent;
10568 	bool inSimpleMode;
10569 
10570 
10571 	///
10572 	this(Widget parent) {
10573 		super(null); // FIXME
10574 		_parts = Parts(this);
10575 		tabStop = false;
10576 		version(win32_widgets) {
10577 			parentWindow = parent.parentWindow;
10578 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
10579 
10580 			RECT rect;
10581 			GetWindowRect(hwnd, &rect);
10582 			idealHeight = rect.bottom - rect.top;
10583 			assert(idealHeight);
10584 		} else version(custom_widgets) {
10585 		} else static assert(false);
10586 	}
10587 
10588 	override void recomputeChildLayout() {
10589 		int remainingLength = this.width;
10590 
10591 		int proportionalSum;
10592 		int proportionalCount;
10593 		foreach(idx, part; this.partsArray) {
10594 			with(Part.WidthUnits)
10595 			final switch(part.units) {
10596 				case DeviceDependentPixels:
10597 					part.currentlyAssignedWidth = part.width;
10598 					remainingLength -= part.currentlyAssignedWidth;
10599 				break;
10600 				case DeviceIndependentPixels:
10601 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
10602 					remainingLength -= part.currentlyAssignedWidth;
10603 				break;
10604 				case ApproximateCharacters:
10605 					auto cs = getComputedStyle();
10606 					auto font = cs.font;
10607 
10608 					part.currentlyAssignedWidth = font.averageWidth * this.width;
10609 					remainingLength -= part.currentlyAssignedWidth;
10610 				break;
10611 				case Proportional:
10612 					proportionalSum += part.width;
10613 					proportionalCount ++;
10614 				break;
10615 			}
10616 		}
10617 
10618 		foreach(part; this.partsArray) {
10619 			if(part.units == Part.WidthUnits.Proportional) {
10620 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
10621 				if(proportion == 0)
10622 					proportion = 1;
10623 
10624 				if(proportionalSum == 0)
10625 					proportionalSum = proportionalCount;
10626 
10627 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
10628 			}
10629 		}
10630 
10631 		super.recomputeChildLayout();
10632 	}
10633 
10634 	version(win32_widgets)
10635 	override protected void dpiChanged() {
10636 		RECT rect;
10637 		GetWindowRect(hwnd, &rect);
10638 		idealHeight = rect.bottom - rect.top;
10639 		assert(idealHeight);
10640 	}
10641 
10642 	version(custom_widgets)
10643 	override void paint(WidgetPainter painter) {
10644 		auto cs = getComputedStyle();
10645 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10646 		int cpos = 0;
10647 		foreach(idx, part; this.partsArray) {
10648 			auto partWidth = part.currentlyAssignedWidth;
10649 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
10650 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
10651 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
10652 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
10653 
10654 			painter.outlineColor = cs.foregroundColor();
10655 			painter.fillColor = cs.foregroundColor();
10656 
10657 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
10658 			cpos += partWidth;
10659 		}
10660 	}
10661 
10662 
10663 	version(win32_widgets) {
10664 		private int idealHeight;
10665 		override int maxHeight() { return idealHeight; }
10666 		override int minHeight() { return idealHeight; }
10667 	} else version(custom_widgets) {
10668 		override int maxHeight() { return defaultLineHeight + 4; }
10669 		override int minHeight() { return defaultLineHeight + 4; }
10670 	} else static assert(false);
10671 }
10672 
10673 /// Displays an in-progress indicator without known values
10674 version(none)
10675 class IndefiniteProgressBar : Widget {
10676 	version(win32_widgets)
10677 	this(Widget parent) {
10678 		super(parent);
10679 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
10680 		tabStop = false;
10681 	}
10682 	override int minHeight() { return 10; }
10683 }
10684 
10685 /// A progress bar with a known endpoint and completion amount
10686 class ProgressBar : Widget {
10687 	/++
10688 		History:
10689 			Added March 16, 2022 (dub v10.7)
10690 	+/
10691 	this(int min, int max, Widget parent) {
10692 		this(parent);
10693 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
10694 	}
10695 	this(Widget parent) {
10696 		version(win32_widgets) {
10697 			super(parent);
10698 			createWin32Window(this, "msctls_progress32"w, "", 0);
10699 			tabStop = false;
10700 		} else version(custom_widgets) {
10701 			super(parent);
10702 			max = 100;
10703 			step = 10;
10704 			tabStop = false;
10705 		} else static assert(0);
10706 	}
10707 
10708 	version(custom_widgets)
10709 	override void paint(WidgetPainter painter) {
10710 		auto cs = getComputedStyle();
10711 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10712 		painter.fillColor = cs.progressBarColor;
10713 		painter.drawRectangle(Point(0, 0), width * current / max, height);
10714 	}
10715 
10716 
10717 	version(custom_widgets) {
10718 		int current;
10719 		int max;
10720 		int step;
10721 	}
10722 
10723 	///
10724 	void advanceOneStep() {
10725 		version(win32_widgets)
10726 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
10727 		else version(custom_widgets)
10728 			addToPosition(step);
10729 		else static assert(false);
10730 	}
10731 
10732 	///
10733 	void setStepIncrement(int increment) {
10734 		version(win32_widgets)
10735 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
10736 		else version(custom_widgets)
10737 			step = increment;
10738 		else static assert(false);
10739 	}
10740 
10741 	///
10742 	void addToPosition(int amount) {
10743 		version(win32_widgets)
10744 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
10745 		else version(custom_widgets)
10746 			setPosition(current + amount);
10747 		else static assert(false);
10748 	}
10749 
10750 	///
10751 	void setPosition(int pos) {
10752 		version(win32_widgets)
10753 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
10754 		else version(custom_widgets) {
10755 			current = pos;
10756 			if(current > max)
10757 				current = max;
10758 			redraw();
10759 		}
10760 		else static assert(false);
10761 	}
10762 
10763 	///
10764 	void setRange(ushort min, ushort max) {
10765 		version(win32_widgets)
10766 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
10767 		else version(custom_widgets) {
10768 			this.max = max;
10769 		}
10770 		else static assert(false);
10771 	}
10772 
10773 	override int minHeight() { return 10; }
10774 }
10775 
10776 version(custom_widgets)
10777 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
10778 	thisLabel.reserve(label.length);
10779 	bool justSawAmpersand;
10780 	foreach(ch; label) {
10781 		if(justSawAmpersand) {
10782 			justSawAmpersand = false;
10783 			if(ch == '&') {
10784 				goto plain;
10785 			}
10786 			thisAccelerator = ch;
10787 		} else {
10788 			if(ch == '&') {
10789 				justSawAmpersand = true;
10790 				continue;
10791 			}
10792 			plain:
10793 			thisLabel ~= ch;
10794 		}
10795 	}
10796 }
10797 
10798 /++
10799 	Creates the fieldset (also known as a group box) with the given label. A fieldset is generally used a container for mutually exclusive [Radiobox]s.
10800 
10801 
10802 	Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN
10803 
10804 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
10805 
10806 	History:
10807 		The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it.
10808 +/
10809 class Fieldset : Widget {
10810 	// FIXME: on Windows,it doesn't draw the background on the label
10811 	// on X, it doesn't fix the clipping rectangle for it
10812 	version(win32_widgets)
10813 		override int paddingTop() { return defaultLineHeight; }
10814 	else version(custom_widgets)
10815 		override int paddingTop() { return defaultLineHeight + 2; }
10816 	else static assert(false);
10817 	override int paddingBottom() { return 6; }
10818 	override int paddingLeft() { return 6; }
10819 	override int paddingRight() { return 6; }
10820 
10821 	override int marginLeft() { return 6; }
10822 	override int marginRight() { return 6; }
10823 	override int marginTop() { return 2; }
10824 	override int marginBottom() { return 2; }
10825 
10826 	string legend;
10827 
10828 	version(custom_widgets) private dchar accelerator;
10829 
10830 	this(string legend, Widget parent) {
10831 		version(win32_widgets) {
10832 			super(parent);
10833 			this.legend = legend;
10834 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
10835 			tabStop = false;
10836 		} else version(custom_widgets) {
10837 			super(parent);
10838 			tabStop = false;
10839 
10840 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
10841 		} else static assert(0);
10842 	}
10843 
10844 	version(custom_widgets)
10845 	override void paint(WidgetPainter painter) {
10846 		auto dlh = defaultLineHeight;
10847 
10848 		painter.fillColor = Color.transparent;
10849 		auto cs = getComputedStyle();
10850 		painter.pen = Pen(cs.foregroundColor, 1);
10851 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
10852 
10853 		auto tx = painter.textSize(legend);
10854 		painter.outlineColor = Color.transparent;
10855 
10856 		version(Windows) {
10857 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
10858 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
10859 			SelectObject(painter.impl.hdc, b);
10860 		} else static if(UsingSimpledisplayX11) {
10861 			painter.fillColor = getComputedStyle().windowBackgroundColor;
10862 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
10863 		}
10864 		painter.outlineColor = cs.foregroundColor;
10865 		painter.drawText(Point(8, 0), legend);
10866 	}
10867 
10868 	override int maxHeight() {
10869 		auto m = paddingTop() + paddingBottom();
10870 		foreach(child; children) {
10871 			auto mh = child.maxHeight();
10872 			if(mh == int.max)
10873 				return int.max;
10874 			m += mh;
10875 			m += child.marginBottom();
10876 			m += child.marginTop();
10877 		}
10878 		m += 6;
10879 		if(m < minHeight)
10880 			return minHeight;
10881 		return m;
10882 	}
10883 
10884 	override int minHeight() {
10885 		auto m = paddingTop() + paddingBottom();
10886 		foreach(child; children) {
10887 			m += child.minHeight();
10888 			m += child.marginBottom();
10889 			m += child.marginTop();
10890 		}
10891 		return m + 6;
10892 	}
10893 
10894 	override int minWidth() {
10895 		return 6 + cast(int) this.legend.length * 7;
10896 	}
10897 }
10898 
10899 /++
10900 	$(IMG //arsdnet.net/minigui-screenshots/windows/Fieldset.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep")
10901 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
10902 +/
10903 version(minigui_screenshots)
10904 @Screenshot("Fieldset")
10905 unittest {
10906 	auto window = new Window(200, 100);
10907 	auto set = new Fieldset("Baby will", window);
10908 	auto option1 = new Radiobox("Eat", set);
10909 	auto option2 = new Radiobox("Cry", set);
10910 	auto option3 = new Radiobox("Sleep", set);
10911 	window.loop();
10912 }
10913 
10914 /// Draws a line
10915 class HorizontalRule : Widget {
10916 	mixin Margin!q{ 2 };
10917 	override int minHeight() { return 2; }
10918 	override int maxHeight() { return 2; }
10919 
10920 	///
10921 	this(Widget parent) {
10922 		super(parent);
10923 	}
10924 
10925 	override void paint(WidgetPainter painter) {
10926 		auto cs = getComputedStyle();
10927 		painter.outlineColor = cs.darkAccentColor;
10928 		painter.drawLine(Point(0, 0), Point(width, 0));
10929 		painter.outlineColor = cs.lightAccentColor;
10930 		painter.drawLine(Point(0, 1), Point(width, 1));
10931 	}
10932 }
10933 
10934 version(minigui_screenshots)
10935 @Screenshot("HorizontalRule")
10936 /++
10937 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
10938 
10939 +/
10940 unittest {
10941 	auto window = new Window(200, 100);
10942 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
10943 	new HorizontalRule(window);
10944 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
10945 	window.loop();
10946 }
10947 
10948 /// ditto
10949 class VerticalRule : Widget {
10950 	mixin Margin!q{ 2 };
10951 	override int minWidth() { return 2; }
10952 	override int maxWidth() { return 2; }
10953 
10954 	///
10955 	this(Widget parent) {
10956 		super(parent);
10957 	}
10958 
10959 	override void paint(WidgetPainter painter) {
10960 		auto cs = getComputedStyle();
10961 		painter.outlineColor = cs.darkAccentColor;
10962 		painter.drawLine(Point(0, 0), Point(0, height));
10963 		painter.outlineColor = cs.lightAccentColor;
10964 		painter.drawLine(Point(1, 0), Point(1, height));
10965 	}
10966 }
10967 
10968 
10969 ///
10970 class Menu : Window {
10971 	void remove() {
10972 		foreach(i, child; parentWindow.children)
10973 			if(child is this) {
10974 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
10975 				break;
10976 			}
10977 		parentWindow.redraw();
10978 
10979 		parentWindow.releaseMouseCapture();
10980 	}
10981 
10982 	///
10983 	void addSeparator() {
10984 		version(win32_widgets)
10985 			AppendMenu(handle, MF_SEPARATOR, 0, null);
10986 		else version(custom_widgets)
10987 			auto hr = new HorizontalRule(this);
10988 		else static assert(0);
10989 	}
10990 
10991 	override int paddingTop() { return 4; }
10992 	override int paddingBottom() { return 4; }
10993 	override int paddingLeft() { return 2; }
10994 	override int paddingRight() { return 2; }
10995 
10996 	version(win32_widgets) {}
10997 	else version(custom_widgets) {
10998 		SimpleWindow dropDown;
10999 		Widget menuParent;
11000 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
11001 			this.menuParent = parent;
11002 
11003 			int w = 150;
11004 			int h = paddingTop + paddingBottom;
11005 			if(this.children.length) {
11006 				// hacking it to get the ideal height out of recomputeChildLayout
11007 				this.width = w;
11008 				this.height = h;
11009 				this.recomputeChildLayoutEntry();
11010 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
11011 				h += paddingBottom;
11012 
11013 				h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances
11014 			}
11015 
11016 			if(offsetY == int.min)
11017 				offsetY = parent.defaultLineHeight;
11018 
11019 			auto coord = parent.globalCoordinates();
11020 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
11021 			this.x = 0;
11022 			this.y = 0;
11023 			this.width = dropDown.width;
11024 			this.height = dropDown.height;
11025 			this.drawableWindow = dropDown;
11026 			this.recomputeChildLayoutEntry();
11027 
11028 			static if(UsingSimpledisplayX11)
11029 				XSync(XDisplayConnection.get, 0);
11030 
11031 			dropDown.visibilityChanged = (bool visible) {
11032 				if(visible) {
11033 					this.redraw();
11034 					dropDown.grabInput();
11035 				} else {
11036 					dropDown.releaseInputGrab();
11037 				}
11038 			};
11039 
11040 			dropDown.show();
11041 
11042 			clickListener = this.addEventListener((scope ClickEvent ev) {
11043 				unpopup();
11044 				// need to unlock asap just in case other user handlers block...
11045 				static if(UsingSimpledisplayX11)
11046 					flushGui();
11047 			}, true /* again for asap action */);
11048 		}
11049 
11050 		EventListener clickListener;
11051 	}
11052 	else static assert(false);
11053 
11054 	version(custom_widgets)
11055 	void unpopup() {
11056 		mouseLastOver = mouseLastDownOn = null;
11057 		dropDown.hide();
11058 		if(!menuParent.parentWindow.win.closed) {
11059 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
11060 				maw.setDynamicState(DynamicState.depressed, false);
11061 				maw.setDynamicState(DynamicState.hover, false);
11062 				maw.redraw();
11063 			}
11064 			// menuParent.parentWindow.win.focus();
11065 		}
11066 		clickListener.disconnect();
11067 	}
11068 
11069 	MenuItem[] items;
11070 
11071 	///
11072 	MenuItem addItem(MenuItem item) {
11073 		addChild(item);
11074 		items ~= item;
11075 		version(win32_widgets) {
11076 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11077 		}
11078 		return item;
11079 	}
11080 
11081 	string label;
11082 
11083 	version(win32_widgets) {
11084 		HMENU handle;
11085 		///
11086 		this(string label, Widget parent) {
11087 			// not actually passing the parent since it effs up the drawing
11088 			super(cast(Widget) null);// parent);
11089 			this.label = label;
11090 			handle = CreatePopupMenu();
11091 		}
11092 	} else version(custom_widgets) {
11093 		///
11094 		this(string label, Widget parent) {
11095 
11096 			if(dropDown) {
11097 				dropDown.close();
11098 			}
11099 			dropDown = new SimpleWindow(
11100 				150, 4,
11101 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
11102 
11103 			this.label = label;
11104 
11105 			super(dropDown);
11106 		}
11107 	} else static assert(false);
11108 
11109 	override int maxHeight() { return defaultLineHeight; }
11110 	override int minHeight() { return defaultLineHeight; }
11111 
11112 	version(custom_widgets)
11113 	override void paint(WidgetPainter painter) {
11114 		this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
11115 	}
11116 }
11117 
11118 /++
11119 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
11120 +/
11121 class MenuItem : MouseActivatedWidget {
11122 	Menu submenu;
11123 
11124 	Action action;
11125 	string label;
11126 
11127 	override int paddingLeft() { return 4; }
11128 
11129 	override int maxHeight() { return defaultLineHeight + 4; }
11130 	override int minHeight() { return defaultLineHeight + 4; }
11131 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
11132 	override int maxWidth() {
11133 		if(cast(MenuBar) parent) {
11134 			return minWidth();
11135 		}
11136 		return int.max;
11137 	}
11138 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
11139 	this(string lbl, Widget parent = null) {
11140 		super(parent);
11141 		//label = lbl; // FIXME
11142 		foreach(char ch; lbl) // FIXME
11143 			if(ch != '&') // FIXME
11144 				label ~= ch; // FIXME
11145 		tabStop = false; // these are selected some other way
11146 	}
11147 
11148 	///
11149 	this(Action action, Widget parent = null) {
11150 		assert(action !is null);
11151 		this(action.label, parent);
11152 		this.action = action;
11153 		tabStop = false; // these are selected some other way
11154 	}
11155 
11156 	version(custom_widgets)
11157 	override void paint(WidgetPainter painter) {
11158 		auto cs = getComputedStyle();
11159 		if(dynamicState & DynamicState.depressed)
11160 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11161 		if(dynamicState & DynamicState.hover)
11162 			painter.outlineColor = cs.activeMenuItemColor;
11163 		else
11164 			painter.outlineColor = cs.foregroundColor;
11165 		painter.fillColor = Color.transparent;
11166 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11167 		if(action && action.accelerator !is KeyEvent.init) {
11168 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
11169 
11170 		}
11171 	}
11172 
11173 	static class Style : Widget.Style {
11174 		override bool variesWithState(ulong dynamicStateFlags) {
11175 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11176 		}
11177 	}
11178 	mixin OverrideStyle!Style;
11179 
11180 	override void defaultEventHandler_triggered(Event event) {
11181 		if(action)
11182 		foreach(handler; action.triggered)
11183 			handler();
11184 
11185 		if(auto pmenu = cast(Menu) this.parent)
11186 			pmenu.remove();
11187 
11188 		super.defaultEventHandler_triggered(event);
11189 	}
11190 }
11191 
11192 version(win32_widgets)
11193 /// A "mouse activiated widget" is really just an abstract variant of button.
11194 class MouseActivatedWidget : Widget {
11195 	@property bool isChecked() {
11196 		assert(hwnd);
11197 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
11198 
11199 	}
11200 	@property void isChecked(bool state) {
11201 		assert(hwnd);
11202 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
11203 
11204 	}
11205 
11206 	override void handleWmCommand(ushort cmd, ushort id) {
11207 		if(cmd == 0) {
11208 			auto event = new Event(EventType.triggered, this);
11209 			event.dispatch();
11210 		}
11211 	}
11212 
11213 	this(Widget parent) {
11214 		super(parent);
11215 	}
11216 }
11217 else version(custom_widgets)
11218 /// ditto
11219 class MouseActivatedWidget : Widget {
11220 	@property bool isChecked() { return isChecked_; }
11221 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
11222 
11223 	private bool isChecked_;
11224 
11225 	this(Widget parent) {
11226 		super(parent);
11227 
11228 		addEventListener((MouseDownEvent ev) {
11229 			if(ev.button == MouseButton.left) {
11230 				setDynamicState(DynamicState.depressed, true);
11231 				setDynamicState(DynamicState.hover, true);
11232 				redraw();
11233 			}
11234 		});
11235 
11236 		addEventListener((MouseUpEvent ev) {
11237 			if(ev.button == MouseButton.left) {
11238 				setDynamicState(DynamicState.depressed, false);
11239 				setDynamicState(DynamicState.hover, false);
11240 				redraw();
11241 			}
11242 		});
11243 
11244 		addEventListener((MouseMoveEvent mme) {
11245 			if(!(mme.state & ModifierState.leftButtonDown)) {
11246 				if(dynamicState_ & DynamicState.depressed) {
11247 					setDynamicState(DynamicState.depressed, false);
11248 					redraw();
11249 				}
11250 			}
11251 		});
11252 	}
11253 
11254 	override void defaultEventHandler_focus(Event ev) {
11255 		super.defaultEventHandler_focus(ev);
11256 		this.redraw();
11257 	}
11258 	override void defaultEventHandler_blur(Event ev) {
11259 		super.defaultEventHandler_blur(ev);
11260 		setDynamicState(DynamicState.depressed, false);
11261 		this.redraw();
11262 	}
11263 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
11264 		super.defaultEventHandler_keydown(ev);
11265 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
11266 			setDynamicState(DynamicState.depressed, true);
11267 			setDynamicState(DynamicState.hover, true);
11268 			this.redraw();
11269 		}
11270 	}
11271 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
11272 		super.defaultEventHandler_keyup(ev);
11273 		if(!(dynamicState & DynamicState.depressed))
11274 			return;
11275 		setDynamicState(DynamicState.depressed, false);
11276 		setDynamicState(DynamicState.hover, false);
11277 		this.redraw();
11278 
11279 		auto event = new Event(EventType.triggered, this);
11280 		event.sendDirectly();
11281 	}
11282 	override void defaultEventHandler_click(ClickEvent ev) {
11283 		super.defaultEventHandler_click(ev);
11284 		if(ev.button == MouseButton.left) {
11285 			auto event = new Event(EventType.triggered, this);
11286 			event.sendDirectly();
11287 		}
11288 	}
11289 
11290 }
11291 else static assert(false);
11292 
11293 /*
11294 /++
11295 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
11296 
11297 	Basically the same as a checkbox.
11298 +/
11299 class OnOffSwitch : MouseActivatedWidget {
11300 
11301 }
11302 */
11303 
11304 /++
11305 	History:
11306 		Added June 15, 2021 (dub v10.1)
11307 +/
11308 struct ImageLabel {
11309 	/++
11310 		Defines a label+image combo used by some widgets.
11311 
11312 		If you provide just a text label, that is all the widget will try to
11313 		display. Or just an image will display just that. If you provide both,
11314 		it may display both text and image side by side or display the image
11315 		and offer text on an input event depending on the widget.
11316 
11317 		History:
11318 			The `alignment` parameter was added on September 27, 2021
11319 	+/
11320 	this(string label, TextAlignment alignment = TextAlignment.Center) {
11321 		this.label = label;
11322 		this.displayFlags = DisplayFlags.displayText;
11323 		this.alignment = alignment;
11324 	}
11325 
11326 	/// ditto
11327 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11328 		this.label = label;
11329 		this.image = image;
11330 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11331 		this.alignment = alignment;
11332 	}
11333 
11334 	/// ditto
11335 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11336 		this.image = image;
11337 		this.displayFlags = DisplayFlags.displayImage;
11338 		this.alignment = alignment;
11339 	}
11340 
11341 	/// ditto
11342 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
11343 		this.label = label;
11344 		this.image = image;
11345 		this.alignment = alignment;
11346 		this.displayFlags = displayFlags;
11347 	}
11348 
11349 	string label;
11350 	MemoryImage image;
11351 
11352 	enum DisplayFlags {
11353 		displayText = 1 << 0,
11354 		displayImage = 1 << 1,
11355 	}
11356 
11357 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11358 
11359 	TextAlignment alignment;
11360 }
11361 
11362 /++
11363 	A basic checked or not checked box with an attached label.
11364 
11365 
11366 	Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN
11367 
11368 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11369 
11370 	History:
11371 		The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it.
11372 +/
11373 class Checkbox : MouseActivatedWidget {
11374 	version(win32_widgets) {
11375 		override int maxHeight() { return scaleWithDpi(16); }
11376 		override int minHeight() { return scaleWithDpi(16); }
11377 	} else version(custom_widgets) {
11378 		private enum buttonSize = 16;
11379 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11380 		override int minHeight() { return maxHeight(); }
11381 	} else static assert(0);
11382 
11383 	override int marginLeft() { return 4; }
11384 
11385 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
11386 
11387 	/++
11388 		Just an alias because I keep typing checked out of web habit.
11389 
11390 		History:
11391 			Added May 31, 2021
11392 	+/
11393 	alias checked = isChecked;
11394 
11395 	private string label;
11396 	private dchar accelerator;
11397 
11398 	/++
11399 	+/
11400 	this(string label, Widget parent) {
11401 		this(ImageLabel(label), Appearance.checkbox, parent);
11402 	}
11403 
11404 	/// ditto
11405 	this(string label, Appearance appearance, Widget parent) {
11406 		this(ImageLabel(label), appearance, parent);
11407 	}
11408 
11409 	/++
11410 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
11411 
11412 		History:
11413 			Added June 29, 2021 (dub v10.2)
11414 	+/
11415 	enum Appearance {
11416 		checkbox, /// a normal checkbox
11417 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
11418 		//sliderswitch,
11419 	}
11420 	private Appearance appearance;
11421 
11422 	/// ditto
11423 	private this(ImageLabel label, Appearance appearance, Widget parent) {
11424 		super(parent);
11425 		version(win32_widgets) {
11426 			this.label = label.label;
11427 
11428 			uint extraStyle;
11429 			final switch(appearance) {
11430 				case Appearance.checkbox:
11431 				break;
11432 				case Appearance.pushbutton:
11433 					extraStyle |= BS_PUSHLIKE;
11434 				break;
11435 			}
11436 
11437 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
11438 		} else version(custom_widgets) {
11439 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
11440 		} else static assert(0);
11441 	}
11442 
11443 	version(custom_widgets)
11444 	override void paint(WidgetPainter painter) {
11445 		auto cs = getComputedStyle();
11446 		if(isFocused()) {
11447 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11448 			painter.fillColor = cs.windowBackgroundColor;
11449 			painter.drawRectangle(Point(0, 0), width, height);
11450 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11451 		} else {
11452 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
11453 			painter.fillColor = cs.windowBackgroundColor;
11454 			painter.drawRectangle(Point(0, 0), width, height);
11455 		}
11456 
11457 
11458 		painter.outlineColor = Color.black;
11459 		painter.fillColor = Color.white;
11460 		enum rectOffset = 2;
11461 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
11462 
11463 		if(isChecked) {
11464 			auto size = scaleWithDpi(2);
11465 			painter.pen = Pen(Color.black, size);
11466 			// I'm using height so the checkbox is square
11467 			enum padding = 3;
11468 			painter.drawLine(
11469 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
11470 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
11471 			);
11472 			painter.drawLine(
11473 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
11474 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
11475 			);
11476 
11477 			painter.pen = Pen(Color.black, 1);
11478 		}
11479 
11480 		if(label !is null) {
11481 			painter.outlineColor = cs.foregroundColor();
11482 			painter.fillColor = cs.foregroundColor();
11483 
11484 			// i want the centerline of the text to be aligned with the centerline of the checkbox
11485 			/+
11486 			auto font = cs.font();
11487 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
11488 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
11489 			+/
11490 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
11491 		}
11492 	}
11493 
11494 	override void defaultEventHandler_triggered(Event ev) {
11495 		isChecked = !isChecked;
11496 
11497 		this.emit!(ChangeEvent!bool)(&isChecked);
11498 
11499 		redraw();
11500 	}
11501 
11502 	/// Emits a change event with the checked state
11503 	mixin Emits!(ChangeEvent!bool);
11504 }
11505 
11506 /// Adds empty space to a layout.
11507 class VerticalSpacer : Widget {
11508 	///
11509 	this(Widget parent) {
11510 		super(parent);
11511 	}
11512 }
11513 
11514 /// ditto
11515 class HorizontalSpacer : Widget {
11516 	///
11517 	this(Widget parent) {
11518 		super(parent);
11519 		this.tabStop = false;
11520 	}
11521 }
11522 
11523 
11524 /++
11525 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
11526 
11527 
11528 	Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN
11529 
11530 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11531 
11532 	History:
11533 		The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it.
11534 +/
11535 class Radiobox : MouseActivatedWidget {
11536 
11537 	version(win32_widgets) {
11538 		override int maxHeight() { return scaleWithDpi(16); }
11539 		override int minHeight() { return scaleWithDpi(16); }
11540 	} else version(custom_widgets) {
11541 		private enum buttonSize = 16;
11542 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11543 		override int minHeight() { return maxHeight(); }
11544 	} else static assert(0);
11545 
11546 	override int marginLeft() { return 4; }
11547 
11548 	// FIXME: make a label getter
11549 	private string label;
11550 	private dchar accelerator;
11551 
11552 	/++
11553 
11554 	+/
11555 	this(string label, Widget parent) {
11556 		super(parent);
11557 		version(win32_widgets) {
11558 			this.label = label;
11559 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
11560 		} else version(custom_widgets) {
11561 			label.extractWindowsStyleLabel(this.label, this.accelerator);
11562 			height = 16;
11563 			width = height + 4 + cast(int) label.length * 16;
11564 		}
11565 	}
11566 
11567 	version(custom_widgets)
11568 	override void paint(WidgetPainter painter) {
11569 		auto cs = getComputedStyle();
11570 
11571 		if(isFocused) {
11572 			painter.fillColor = cs.windowBackgroundColor;
11573 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11574 		} else {
11575 			painter.fillColor = cs.windowBackgroundColor;
11576 			painter.outlineColor = cs.windowBackgroundColor;
11577 		}
11578 		painter.drawRectangle(Point(0, 0), width, height);
11579 
11580 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11581 
11582 		painter.outlineColor = Color.black;
11583 		painter.fillColor = Color.white;
11584 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
11585 		if(isChecked) {
11586 			painter.outlineColor = Color.black;
11587 			painter.fillColor = Color.black;
11588 			// I'm using height so the checkbox is square
11589 			auto size = scaleWithDpi(2);
11590 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
11591 		}
11592 
11593 		painter.outlineColor = cs.foregroundColor();
11594 		painter.fillColor = cs.foregroundColor();
11595 
11596 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11597 	}
11598 
11599 
11600 	override void defaultEventHandler_triggered(Event ev) {
11601 		isChecked = true;
11602 
11603 		if(this.parent) {
11604 			foreach(child; this.parent.children) {
11605 				if(child is this) continue;
11606 				if(auto rb = cast(Radiobox) child) {
11607 					rb.isChecked = false;
11608 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
11609 					rb.redraw();
11610 				}
11611 			}
11612 		}
11613 
11614 		this.emit!(ChangeEvent!bool)(&this.isChecked);
11615 
11616 		redraw();
11617 	}
11618 
11619 	/// Emits a change event with if it is checked. Note that when you select one in a group, that one will emit changed with value == true, and the previous one will emit changed with value == false right before. A button group may catch this and change the event.
11620 	mixin Emits!(ChangeEvent!bool);
11621 }
11622 
11623 
11624 /++
11625 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
11626 
11627 
11628 	Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN
11629 
11630 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11631 
11632 	History:
11633 		The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it.
11634 +/
11635 class Button : MouseActivatedWidget {
11636 	override int heightStretchiness() { return 3; }
11637 	override int widthStretchiness() { return 3; }
11638 
11639 	/++
11640 		If true, this button will emit trigger events on double (and other quick events, if added) click events as well as on normal single click events.
11641 
11642 		History:
11643 			Added July 2, 2021
11644 	+/
11645 	public bool triggersOnMultiClick;
11646 
11647 	private string label_;
11648 	private TextAlignment alignment;
11649 	private dchar accelerator;
11650 
11651 	///
11652 	string label() { return label_; }
11653 	///
11654 	void label(string l) {
11655 		label_ = l;
11656 		version(win32_widgets) {
11657 			WCharzBuffer bfr = WCharzBuffer(l);
11658 			SetWindowTextW(hwnd, bfr.ptr);
11659 		} else version(custom_widgets) {
11660 			redraw();
11661 		}
11662 	}
11663 
11664 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
11665 		super.defaultEventHandler_dblclick(ev);
11666 		if(triggersOnMultiClick) {
11667 			if(ev.button == MouseButton.left) {
11668 				auto event = new Event(EventType.triggered, this);
11669 				event.sendDirectly();
11670 			}
11671 		}
11672 	}
11673 
11674 	private Sprite sprite;
11675 	private int displayFlags;
11676 
11677 	/++
11678 		Creates a push button with the given label, which may be an image or some text.
11679 
11680 		Bugs:
11681 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
11682 
11683 		History:
11684 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
11685 
11686 			The button with label and image will respect requests to show both on Windows as
11687 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
11688 	+/
11689 	this(ImageLabel label, Widget parent) {
11690 		version(win32_widgets) {
11691 			// FIXME: use ideal button size instead
11692 			width = 50;
11693 			height = 30;
11694 			super(parent);
11695 
11696 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
11697 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
11698 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
11699 
11700 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
11701 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
11702 
11703 			if(label.image) {
11704 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
11705 
11706 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
11707 			}
11708 
11709 			this.label = label.label;
11710 		} else version(custom_widgets) {
11711 			width = 50;
11712 			height = 30;
11713 			super(parent);
11714 
11715 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
11716 
11717 			if(label.image) {
11718 				this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
11719 				this.displayFlags = label.displayFlags;
11720 			}
11721 
11722 			this.alignment = label.alignment;
11723 		}
11724 	}
11725 
11726 	///
11727 	this(string label, Widget parent) {
11728 		this(ImageLabel(label), parent);
11729 	}
11730 
11731 	override int minHeight() { return defaultLineHeight + 4; }
11732 
11733 	static class Style : Widget.Style {
11734 		override WidgetBackground background() {
11735 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
11736 
11737 			auto pressed = DynamicState.depressed | DynamicState.hover;
11738 			if((widget.dynamicState & pressed) == pressed) {
11739 				return WidgetBackground(cs.depressedButtonColor());
11740 			} else if(widget.dynamicState & DynamicState.hover) {
11741 				return WidgetBackground(cs.hoveringColor());
11742 			} else {
11743 				return WidgetBackground(cs.buttonColor());
11744 			}
11745 		}
11746 
11747 		override FrameStyle borderStyle() {
11748 			auto pressed = DynamicState.depressed | DynamicState.hover;
11749 			if((widget.dynamicState & pressed) == pressed) {
11750 				return FrameStyle.sunk;
11751 			} else {
11752 				return FrameStyle.risen;
11753 			}
11754 
11755 		}
11756 
11757 		override bool variesWithState(ulong dynamicStateFlags) {
11758 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11759 		}
11760 	}
11761 	mixin OverrideStyle!Style;
11762 
11763 	version(custom_widgets)
11764 	override void paint(WidgetPainter painter) {
11765 		painter.drawThemed(delegate Rectangle(const Rectangle bounds) {
11766 			if(sprite) {
11767 				sprite.drawAt(
11768 					painter,
11769 					bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
11770 					Point(0, 0)
11771 				);
11772 			} else {
11773 				painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
11774 			}
11775 			return bounds;
11776 		});
11777 	}
11778 
11779 	override int flexBasisWidth() {
11780 		version(win32_widgets) {
11781 			SIZE size;
11782 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11783 			if(size.cx == 0)
11784 				goto fallback;
11785 			return size.cx + scaleWithDpi(16);
11786 		}
11787 		fallback:
11788 			return scaleWithDpi(cast(int) label.length * 8 + 16);
11789 	}
11790 
11791 	override int flexBasisHeight() {
11792 		version(win32_widgets) {
11793 			SIZE size;
11794 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11795 			if(size.cy == 0)
11796 				goto fallback;
11797 			return size.cy + scaleWithDpi(6);
11798 		}
11799 		fallback:
11800 			return defaultLineHeight + 4;
11801 	}
11802 }
11803 
11804 /++
11805 	A button with a consistent size, suitable for user commands like OK and CANCEL.
11806 +/
11807 class CommandButton : Button {
11808 	this(string label, Widget parent) {
11809 		super(label, parent);
11810 	}
11811 
11812 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
11813 
11814 	override int maxHeight() {
11815 		return defaultLineHeight + 4;
11816 	}
11817 
11818 	override int maxWidth() {
11819 		return defaultLineHeight * 4;
11820 	}
11821 
11822 	override int marginLeft() { return 12; }
11823 	override int marginRight() { return 12; }
11824 	override int marginTop() { return 12; }
11825 	override int marginBottom() { return 12; }
11826 }
11827 
11828 ///
11829 enum ArrowDirection {
11830 	left, ///
11831 	right, ///
11832 	up, ///
11833 	down ///
11834 }
11835 
11836 ///
11837 version(custom_widgets)
11838 class ArrowButton : Button {
11839 	///
11840 	this(ArrowDirection direction, Widget parent) {
11841 		super("", parent);
11842 		this.direction = direction;
11843 		triggersOnMultiClick = true;
11844 	}
11845 
11846 	private ArrowDirection direction;
11847 
11848 	override int minHeight() { return scaleWithDpi(16); }
11849 	override int maxHeight() { return scaleWithDpi(16); }
11850 	override int minWidth() { return scaleWithDpi(16); }
11851 	override int maxWidth() { return scaleWithDpi(16); }
11852 
11853 	override void paint(WidgetPainter painter) {
11854 		super.paint(painter);
11855 
11856 		auto cs = getComputedStyle();
11857 
11858 		painter.outlineColor = cs.foregroundColor;
11859 		painter.fillColor = cs.foregroundColor;
11860 
11861 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
11862 
11863 		final switch(direction) {
11864 			case ArrowDirection.up:
11865 				painter.drawPolygon(
11866 					scaleWithDpi(Point(2, 10) + offset),
11867 					scaleWithDpi(Point(7, 5) + offset),
11868 					scaleWithDpi(Point(12, 10) + offset),
11869 					scaleWithDpi(Point(2, 10) + offset)
11870 				);
11871 			break;
11872 			case ArrowDirection.down:
11873 				painter.drawPolygon(
11874 					scaleWithDpi(Point(2, 6) + offset),
11875 					scaleWithDpi(Point(7, 11) + offset),
11876 					scaleWithDpi(Point(12, 6) + offset),
11877 					scaleWithDpi(Point(2, 6) + offset)
11878 				);
11879 			break;
11880 			case ArrowDirection.left:
11881 				painter.drawPolygon(
11882 					scaleWithDpi(Point(10, 2) + offset),
11883 					scaleWithDpi(Point(5, 7) + offset),
11884 					scaleWithDpi(Point(10, 12) + offset),
11885 					scaleWithDpi(Point(10, 2) + offset)
11886 				);
11887 			break;
11888 			case ArrowDirection.right:
11889 				painter.drawPolygon(
11890 					scaleWithDpi(Point(6, 2) + offset),
11891 					scaleWithDpi(Point(11, 7) + offset),
11892 					scaleWithDpi(Point(6, 12) + offset),
11893 					scaleWithDpi(Point(6, 2) + offset)
11894 				);
11895 			break;
11896 		}
11897 	}
11898 }
11899 
11900 private
11901 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
11902 	int x, y;
11903 	Widget par = c;
11904 	while(par) {
11905 		x += par.x;
11906 		y += par.y;
11907 		par = par.parent;
11908 	}
11909 	return [x, y];
11910 }
11911 
11912 version(win32_widgets)
11913 private
11914 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
11915 // MapWindowPoints?
11916 	int x, y;
11917 	Widget par = c;
11918 	while(par) {
11919 		x += par.x;
11920 		y += par.y;
11921 		par = par.parent;
11922 		if(par !is null && par.useNativeDrawing())
11923 			break;
11924 	}
11925 	return [x, y];
11926 }
11927 
11928 ///
11929 class ImageBox : Widget {
11930 	private MemoryImage image_;
11931 
11932 	override int widthStretchiness() { return 1; }
11933 	override int heightStretchiness() { return 1; }
11934 	override int widthShrinkiness() { return 1; }
11935 	override int heightShrinkiness() { return 1; }
11936 
11937 	override int flexBasisHeight() {
11938 		return image_.height;
11939 	}
11940 
11941 	override int flexBasisWidth() {
11942 		return image_.width;
11943 	}
11944 
11945 	///
11946 	public void setImage(MemoryImage image){
11947 		this.image_ = image;
11948 		if(this.parentWindow && this.parentWindow.win) {
11949 			if(sprite)
11950 				sprite.dispose();
11951 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
11952 		}
11953 		redraw();
11954 	}
11955 
11956 	/// How to fit the image in the box if they aren't an exact match in size?
11957 	enum HowToFit {
11958 		center, /// centers the image, cropping around all the edges as needed
11959 		crop, /// always draws the image in the upper left, cropping the lower right if needed
11960 		// stretch, /// not implemented
11961 	}
11962 
11963 	private Sprite sprite;
11964 	private HowToFit howToFit_;
11965 
11966 	private Color backgroundColor_;
11967 
11968 	///
11969 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
11970 		this.image_ = image;
11971 		this.tabStop = false;
11972 		this.howToFit_ = howToFit;
11973 		this.backgroundColor_ = backgroundColor;
11974 		super(parent);
11975 		updateSprite();
11976 	}
11977 
11978 	/// ditto
11979 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
11980 		this(image, howToFit, Color.transparent, parent);
11981 	}
11982 
11983 	private void updateSprite() {
11984 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
11985 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
11986 		}
11987 	}
11988 
11989 	override void paint(WidgetPainter painter) {
11990 		updateSprite();
11991 		if(backgroundColor_.a) {
11992 			painter.fillColor = backgroundColor_;
11993 			painter.drawRectangle(Point(0, 0), width, height);
11994 		}
11995 		if(howToFit_ == HowToFit.crop)
11996 			sprite.drawAt(painter, Point(0, 0));
11997 		else if(howToFit_ == HowToFit.center) {
11998 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
11999 		}
12000 	}
12001 }
12002 
12003 ///
12004 class TextLabel : Widget {
12005 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
12006 	override int maxHeight() { return minHeight; }
12007 	override int minWidth() { return 32; }
12008 
12009 	override int flexBasisHeight() { return minHeight(); }
12010 	override int flexBasisWidth() { return defaultTextWidth(label); }
12011 
12012 	string label_;
12013 
12014 	/++
12015 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
12016 
12017 		In practice this means a click on the label will focus the `labelFor`. In future versions
12018 		it will also set screen reader hints but that is not yet implemented.
12019 
12020 		History:
12021 			Added October 3, 2021 (dub v10.4)
12022 	+/
12023 	Widget labelFor;
12024 
12025 	///
12026 	@scriptable
12027 	string label() { return label_; }
12028 
12029 	///
12030 	@scriptable
12031 	void label(string l) {
12032 		label_ = l;
12033 		version(win32_widgets) {
12034 			WCharzBuffer bfr = WCharzBuffer(l);
12035 			SetWindowTextW(hwnd, bfr.ptr);
12036 		} else version(custom_widgets)
12037 			redraw();
12038 	}
12039 
12040 	override void defaultEventHandler_click(scope ClickEvent ce) {
12041 		if(this.labelFor !is null)
12042 			this.labelFor.focus();
12043 	}
12044 
12045 	/++
12046 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
12047 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
12048 	+/
12049 	this(string label, TextAlignment alignment, Widget parent) {
12050 		this.label_ = label;
12051 		this.alignment = alignment;
12052 		this.tabStop = false;
12053 		super(parent);
12054 
12055 		version(win32_widgets)
12056 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
12057 	}
12058 
12059 	/// ditto
12060 	this(string label, Widget parent) {
12061 		this(label, TextAlignment.Right, parent);
12062 	}
12063 
12064 	TextAlignment alignment;
12065 
12066 	version(custom_widgets)
12067 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12068 		painter.outlineColor = getComputedStyle().foregroundColor;
12069 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
12070 		return bounds;
12071 	}
12072 
12073 }
12074 
12075 version(custom_widgets)
12076 	private struct etc {
12077 		mixin ExperimentalTextComponent;
12078 	}
12079 
12080 version(win32_widgets) {
12081 	alias EditableTextWidgetParent = Widget; ///
12082 	version=use_new_text_system;
12083 	import arsd.textlayouter;
12084 } else version(custom_widgets) {
12085 	version(trash_text) {
12086 		alias EditableTextWidgetParent = ScrollableWidget; ///
12087 	} else {
12088 		alias EditableTextWidgetParent = Widget;
12089 		version=use_new_text_system;
12090 		import arsd.textlayouter;
12091 	}
12092 } else static assert(0);
12093 
12094 version(use_new_text_system)
12095 class TextDisplayHelper : Widget {
12096 	protected TextLayouter l;
12097 	protected ScrollMessageWidget smw;
12098 
12099 	private const(TextLayouter.State)*[] undoStack;
12100 	private const(TextLayouter.State)*[] redoStack;
12101 
12102 	private string preservedPrimaryText;
12103 	protected void selectionChanged() {
12104 		static if(UsingSimpledisplayX11)
12105 		with(l.selection()) {
12106 			if(!isEmpty()) {
12107 				getPrimarySelection(parentWindow.win, (in char[] txt) {
12108 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
12109 					if(txt.length) {
12110 						preservedPrimaryText = txt.idup;
12111 						// writeln(preservedPrimaryText);
12112 					}
12113 
12114 					setPrimarySelection(parentWindow.win, getContentString());
12115 				});
12116 			}
12117 		}
12118 	}
12119 
12120 
12121 	bool readonly;
12122 	bool caretNavigation; // scroll lock can flip this
12123 	bool singleLine;
12124 	bool acceptsTabInput;
12125 
12126 	private Menu ctx;
12127 	override Menu contextMenu(int x, int y) {
12128 		if(ctx is null) {
12129 			ctx = new Menu("Actions", this);
12130 			if(!readonly) {
12131 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
12132 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
12133 				ctx.addSeparator();
12134 			}
12135 			if(!readonly)
12136 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
12137 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
12138 			if(!readonly)
12139 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
12140 			if(!readonly)
12141 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
12142 			ctx.addSeparator();
12143 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
12144 		}
12145 		return ctx;
12146 	}
12147 
12148 	override void defaultEventHandler_blur(Event ev) {
12149 		super.defaultEventHandler_blur(ev);
12150 		if(l.wasMutated()) {
12151 			auto evt = new ChangeEvent!string(this, &this.content);
12152 			evt.dispatch();
12153 			l.clearWasMutatedFlag();
12154 		}
12155 	}
12156 
12157 	private string content() {
12158 		return l.getTextString();
12159 	}
12160 
12161 	void undo() {
12162 		if(readonly) return;
12163 		if(undoStack.length) {
12164 			auto state = undoStack[$-1];
12165 			undoStack = undoStack[0 .. $-1];
12166 			undoStack.assumeSafeAppend();
12167 			redoStack ~= l.saveState();
12168 			l.restoreState(state);
12169 			adjustScrollbarSizes();
12170 			scrollForCaret();
12171 			redraw();
12172 			stateCheckpoint = true;
12173 		}
12174 	}
12175 
12176 	void redo() {
12177 		if(readonly) return;
12178 		if(redoStack.length) {
12179 			doStateCheckpoint();
12180 			auto state = redoStack[$-1];
12181 			redoStack = redoStack[0 .. $-1];
12182 			redoStack.assumeSafeAppend();
12183 			l.restoreState(state);
12184 			adjustScrollbarSizes();
12185 			scrollForCaret();
12186 			redraw();
12187 			stateCheckpoint = true;
12188 		}
12189 	}
12190 
12191 	void cut() {
12192 		if(readonly) return;
12193 		with(l.selection()) {
12194 			if(!isEmpty()) {
12195 				setClipboardText(parentWindow.win, getContentString());
12196 				doStateCheckpoint();
12197 				replaceContent("");
12198 				adjustScrollbarSizes();
12199 				scrollForCaret();
12200 				this.redraw();
12201 			}
12202 		}
12203 
12204 	}
12205 
12206 	void copy() {
12207 		with(l.selection()) {
12208 			if(!isEmpty()) {
12209 				setClipboardText(parentWindow.win, getContentString());
12210 				this.redraw();
12211 			}
12212 		}
12213 	}
12214 
12215 	void paste() {
12216 		if(readonly) return;
12217 		getClipboardText(parentWindow.win, (txt) {
12218 			doStateCheckpoint();
12219 			l.selection.replaceContent(txt);
12220 			adjustScrollbarSizes();
12221 			scrollForCaret();
12222 			this.redraw();
12223 		});
12224 	}
12225 
12226 	void deleteContentOfSelection() {
12227 		if(readonly) return;
12228 		doStateCheckpoint();
12229 		l.selection.replaceContent("");
12230 		l.selection.setUserXCoordinate();
12231 		adjustScrollbarSizes();
12232 		scrollForCaret();
12233 		redraw();
12234 	}
12235 
12236 	void selectAll() {
12237 		with(l.selection) {
12238 			moveToStartOfDocument();
12239 			setAnchor();
12240 			moveToEndOfDocument();
12241 			setFocus();
12242 
12243 			selectionChanged();
12244 		}
12245 		redraw();
12246 	}
12247 
12248 	protected bool stateCheckpoint = true;
12249 
12250 	protected void doStateCheckpoint() {
12251 		if(stateCheckpoint) {
12252 			undoStack ~= l.saveState();
12253 			stateCheckpoint = false;
12254 		}
12255 	}
12256 
12257 	protected void adjustScrollbarSizes() {
12258 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
12259 		auto borderWidth = 2;
12260 		this.smw.setTotalArea(l.width, l.height);
12261 		this.smw.setViewableArea(
12262 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
12263 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
12264 	}
12265 
12266 	protected void scrollForCaret() {
12267 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
12268 		smw.scrollIntoView(l.selection.focusBoundingBox());
12269 	}
12270 
12271 	// FIXME: this should be a theme changed event listener instead
12272 	private BaseVisualTheme currentTheme;
12273 	override void recomputeChildLayout() {
12274 		if(currentTheme is null)
12275 			currentTheme = WidgetPainter.visualTheme;
12276 		if(WidgetPainter.visualTheme !is currentTheme) {
12277 			currentTheme = WidgetPainter.visualTheme;
12278 			auto ds = this.l.defaultStyle;
12279 			if(auto ms = cast(MyTextStyle) ds) {
12280 				auto cs = getComputedStyle();
12281 				auto font = cs.font();
12282 				if(font !is null)
12283 					ms.font_ = font;
12284 				else {
12285 					auto osc = new OperatingSystemFont();
12286 					osc.loadDefault;
12287 					ms.font_ = osc;
12288 				}
12289 			}
12290 		}
12291 		super.recomputeChildLayout();
12292 	}
12293 
12294 	private Point adjustForSingleLine(Point p) {
12295 		if(singleLine)
12296 			return Point(p.x, this.height / 2);
12297 		else
12298 			return p;
12299 	}
12300 
12301 	private bool wordWrapEnabled_;
12302 
12303 	this(TextLayouter l, ScrollMessageWidget parent) {
12304 		this.smw = parent;
12305 
12306 		smw.addDefaultWheelListeners(16, 16, 8);
12307 		smw.movementPerButtonClick(16, 16);
12308 
12309 		this.defaultPadding = Rectangle(2, 2, 2, 2);
12310 
12311 		this.l = l;
12312 		super(parent);
12313 
12314 		smw.addEventListener((scope ScrollEvent se) {
12315 			this.redraw();
12316 		});
12317 
12318 		bool mouseDown;
12319 		bool mouseActuallyMoved;
12320 
12321 		this.addEventListener((scope ResizeEvent re) {
12322 			// FIXME: I should add a method to give this client area width thing
12323 			if(wordWrapEnabled_)
12324 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
12325 
12326 			adjustScrollbarSizes();
12327 			scrollForCaret();
12328 
12329 			this.redraw();
12330 		});
12331 
12332 		this.addEventListener((scope KeyDownEvent kde) {
12333 			switch(kde.key) {
12334 				case Key.Up, Key.Down, Key.Left, Key.Right:
12335 				case Key.Home, Key.End:
12336 					stateCheckpoint = true;
12337 					bool setPosition = false;
12338 					switch(kde.key) {
12339 						case Key.Up: l.selection.moveUp(); break;
12340 						case Key.Down: l.selection.moveDown(); break;
12341 						case Key.Left: l.selection.moveLeft(); setPosition = true; break;
12342 						case Key.Right: l.selection.moveRight(); setPosition = true; break;
12343 						case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
12344 						case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
12345 						default: assert(0);
12346 					}
12347 
12348 					if(kde.shiftKey)
12349 						l.selection.setFocus();
12350 					else
12351 						l.selection.setAnchor();
12352 
12353 					selectionChanged();
12354 
12355 					if(setPosition)
12356 						l.selection.setUserXCoordinate();
12357 					scrollForCaret();
12358 					redraw();
12359 				break;
12360 				case Key.PageUp, Key.PageDown:
12361 					// FIXME
12362 					scrollForCaret();
12363 				break;
12364 				case Key.Delete:
12365 					if(l.selection.isEmpty()) {
12366 						l.selection.setAnchor();
12367 						l.selection.moveRight();
12368 						l.selection.setFocus();
12369 					}
12370 					deleteContentOfSelection();
12371 					adjustScrollbarSizes();
12372 					scrollForCaret();
12373 				break;
12374 				case Key.Insert:
12375 				break;
12376 				case Key.A:
12377 					if(kde.ctrlKey)
12378 						selectAll();
12379 				break;
12380 				case Key.F:
12381 					// find
12382 				break;
12383 				case Key.Z:
12384 					if(kde.ctrlKey)
12385 						undo();
12386 				break;
12387 				case Key.R:
12388 					if(kde.ctrlKey)
12389 						redo();
12390 				break;
12391 				case Key.X:
12392 					if(kde.ctrlKey)
12393 						cut();
12394 				break;
12395 				case Key.C:
12396 					if(kde.ctrlKey)
12397 						copy();
12398 				break;
12399 				case Key.V:
12400 					if(kde.ctrlKey)
12401 						paste();
12402 				break;
12403 				case Key.F1:
12404 					with(l.selection()) {
12405 						moveToStartOfLine();
12406 						setAnchor();
12407 						moveToEndOfLine();
12408 						moveToIncludeAdjacentEndOfLineMarker();
12409 						setFocus();
12410 						replaceContent("");
12411 					}
12412 
12413 					redraw();
12414 				break;
12415 				/*
12416 				case Key.F2:
12417 					l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
12418 						//(cast(MyTextStyle) old).font,
12419 						font2,
12420 						Color.red)));
12421 					redraw();
12422 				break;
12423 				*/
12424 				case Key.Tab:
12425 					// we process the char event, so don't want to change focus on it
12426 					if(acceptsTabInput)
12427 						kde.preventDefault();
12428 				break;
12429 				default:
12430 			}
12431 		});
12432 
12433 		Point downAt;
12434 
12435 		static if(UsingSimpledisplayX11)
12436 		this.addEventListener((scope ClickEvent ce) {
12437 			if(ce.button == MouseButton.middle) {
12438 				parentWindow.win.getPrimarySelection((txt) {
12439 					doStateCheckpoint();
12440 
12441 					// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
12442 
12443 					if(txt == l.selection.getContentString && preservedPrimaryText.length)
12444 						l.selection.replaceContent(preservedPrimaryText);
12445 					else
12446 						l.selection.replaceContent(txt);
12447 					redraw();
12448 				});
12449 			}
12450 		});
12451 
12452 		this.addEventListener((scope DoubleClickEvent dce) {
12453 			if(dce.button == MouseButton.left) {
12454 				with(l.selection()) {
12455 					scope dg = delegate const(char)[] (scope return const(char)[] ch) {
12456 						if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
12457 							return ch;
12458 						return null;
12459 					};
12460 					find(dg, 1, true).moveToEnd.setAnchor;
12461 					find(dg, 1, false).moveTo.setFocus;
12462 					selectionChanged();
12463 					redraw();
12464 				}
12465 			}
12466 		});
12467 
12468 		this.addEventListener((scope MouseDownEvent ce) {
12469 			if(ce.button == MouseButton.left) {
12470 				downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12471 				l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
12472 				l.selection.setAnchor();
12473 				mouseDown = true;
12474 				mouseActuallyMoved = false;
12475 				parentWindow.captureMouse(this);
12476 				this.redraw();
12477 			} else if(ce.button == MouseButton.right) {
12478 				this.showContextMenu(ce.clientX, ce.clientY);
12479 			}
12480 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12481 		});
12482 
12483 		Timer autoscrollTimer;
12484 		int autoscrollDirection;
12485 		int autoscrollAmount;
12486 
12487 		void autoscroll() {
12488 			switch(autoscrollDirection) {
12489 				case 0: smw.scrollUp(autoscrollAmount); break;
12490 				case 1: smw.scrollDown(autoscrollAmount); break;
12491 				case 2: smw.scrollLeft(autoscrollAmount); break;
12492 				case 3: smw.scrollRight(autoscrollAmount); break;
12493 				default: assert(0);
12494 			}
12495 
12496 			this.redraw();
12497 		}
12498 
12499 		void setAutoscrollTimer(int direction, int amount) {
12500 			if(autoscrollTimer is null) {
12501 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
12502 			}
12503 
12504 			autoscrollDirection = direction;
12505 			autoscrollAmount = amount;
12506 		}
12507 
12508 		void stopAutoscrollTimer() {
12509 			if(autoscrollTimer !is null) {
12510 				autoscrollTimer.dispose();
12511 				autoscrollTimer = null;
12512 			}
12513 			autoscrollAmount = 0;
12514 			autoscrollDirection = 0;
12515 		}
12516 
12517 		this.addEventListener((scope MouseMoveEvent ce) {
12518 			if(mouseDown) {
12519 				auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12520 
12521 				// FIXME: when scrolling i actually do want a timer.
12522 				// i also want a zone near the sides of the window where i can auto scroll
12523 
12524 				auto scrollMultiplier = scaleWithDpi(16);
12525 				auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
12526 
12527 				if(!singleLine && movedTo.y < 4) {
12528 					setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
12529 				} else
12530 				if(!singleLine && (movedTo.y + 6) > this.height) {
12531 					setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
12532 				} else
12533 				if(movedTo.x < 4) {
12534 					setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
12535 				} else
12536 				if((movedTo.x + 6) > this.width) {
12537 					setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
12538 				} else
12539 					stopAutoscrollTimer();
12540 
12541 				l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
12542 				l.selection.setFocus();
12543 				mouseActuallyMoved = true;
12544 				this.redraw();
12545 			}
12546 		});
12547 
12548 		this.addEventListener((scope MouseUpEvent ce) {
12549 			// FIXME: assert primary selection
12550 			if(mouseDown && ce.button == MouseButton.left) {
12551 				stateCheckpoint = true;
12552 				//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
12553 				//l.selection.setFocus();
12554 				mouseDown = false;
12555 				parentWindow.releaseMouseCapture();
12556 				stopAutoscrollTimer();
12557 				this.redraw();
12558 
12559 				if(mouseActuallyMoved)
12560 					selectionChanged();
12561 			}
12562 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12563 		});
12564 
12565 		this.addEventListener((scope CharEvent ce) {
12566 			if(readonly)
12567 				return;
12568 			if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
12569 				return; // skip the ctrl+x characters we don't care about as plain text
12570 
12571 			if(singleLine && ce.character == '\n')
12572 				return;
12573 			if(!acceptsTabInput && ce.character == '\t')
12574 				return;
12575 
12576 			doStateCheckpoint();
12577 
12578 			char[4] buffer;
12579 			import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web
12580 			auto stride = encode(buffer, ce.character);
12581 			l.selection.replaceContent(buffer[0 .. stride]);
12582 			l.selection.setUserXCoordinate();
12583 			adjustScrollbarSizes();
12584 			scrollForCaret();
12585 			redraw();
12586 		});
12587 	}
12588 
12589 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
12590 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
12591 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidgetParent
12592 		if(parent && parent.parent && parent.parent.parent)
12593 			parent.parent.parent.useStyleProperties(dg);
12594 		else
12595 			super.useStyleProperties(dg);
12596 	}
12597 
12598 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
12599 	override int maxHeight() {
12600 		if(singleLine)
12601 			return minHeight;
12602 		else
12603 			return super.maxHeight();
12604 	}
12605 
12606 	void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
12607 		painter.drawText(upperLeft, text);
12608 	}
12609 
12610 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12611 		//painter.setFont(font);
12612 
12613 		auto cs = getComputedStyle();
12614 		auto defaultColor = cs.foregroundColor;
12615 
12616 		auto old = painter.setClipRectangle(bounds);
12617 		scope(exit) painter.setClipRectangle(old);
12618 
12619 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
12620 			//writeln("Segment: ", txt);
12621 			assert(style !is null);
12622 
12623 			auto myStyle = cast(MyTextStyle) style;
12624 			assert(myStyle !is null);
12625 
12626 			painter.setFont(myStyle.font);
12627 			// defaultColor = myStyle.color; // FIXME: so wrong
12628 
12629 			if(info.selections && info.boundingBox.width > 0) {
12630 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
12631 				painter.fillColor = color;
12632 				painter.outlineColor = color;
12633 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
12634 				painter.outlineColor = cs.selectionForegroundColor;
12635 				//painter.fillColor = Color.white;
12636 			} else {
12637 				painter.outlineColor = defaultColor;
12638 			}
12639 
12640 			if(this.isFocused)
12641 			foreach(idx, caret; carets) {
12642 				if(idx == 0)
12643 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
12644 				painter.drawLine(
12645 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
12646 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
12647 				);
12648 			}
12649 
12650 			if(txt.stripInternal.length) {
12651 				drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
12652 			}
12653 
12654 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
12655 				return false;
12656 			} else {
12657 				return true;
12658 			}
12659 		}, Rectangle(smw.position(), bounds.size));
12660 
12661 		/+
12662 		int place = 0;
12663 		int y = 75;
12664 		foreach(width; widths) {
12665 			painter.fillColor = Color.red;
12666 			painter.drawRectangle(Point(place, y), Size(width, 75));
12667 			//y += 15;
12668 			place += width;
12669 		}
12670 		+/
12671 
12672 		return bounds;
12673 	}
12674 
12675 	static class MyTextStyle : TextStyle {
12676 		OperatingSystemFont font_;
12677 		this(OperatingSystemFont font, bool passwordMode = false) {
12678 			this.font_ = font;
12679 		}
12680 
12681 		override OperatingSystemFont font() {
12682 			return font_;
12683 		}
12684 	}
12685 }
12686 
12687 /+
12688 version(use_new_text_system)
12689 class TextWidget : Widget {
12690 	TextLayouter l;
12691 	ScrollMessageWidget smw;
12692 	TextDisplayHelper helper;
12693 	this(TextLayouter l, Widget parent) {
12694 		this.l = l;
12695 		super(parent);
12696 
12697 		smw = new ScrollMessageWidget(this);
12698 		//smw.horizontalScrollBar.hide;
12699 		//smw.verticalScrollBar.hide;
12700 		smw.addDefaultWheelListeners(16, 16, 8);
12701 		smw.movementPerButtonClick(16, 16);
12702 		helper = new TextDisplayHelper(l, smw);
12703 
12704 		// no need to do this here since there's gonna be a resize
12705 		// event immediately before any drawing
12706 		// smw.setTotalArea(l.width, l.height);
12707 		smw.setViewableArea(
12708 			this.width - this.paddingLeft - this.paddingRight,
12709 			this.height - this.paddingTop - this.paddingBottom);
12710 
12711 		/+
12712 		writeln(l.width, "x", l.height);
12713 		+/
12714 	}
12715 }
12716 +/
12717 
12718 
12719 
12720 
12721 /+
12722 	This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too
12723 +/
12724 
12725 /// Contains the implementation of text editing
12726 abstract class EditableTextWidget : EditableTextWidgetParent {
12727 	this(Widget parent) {
12728 		version(custom_widgets)
12729 			this(true, parent);
12730 		else
12731 			this(false, parent);
12732 	}
12733 
12734 	private bool useCustomWidget;
12735 
12736 	this(bool useCustomWidget, Widget parent) {
12737 		this.useCustomWidget = useCustomWidget;
12738 
12739 		super(parent);
12740 
12741 		if(useCustomWidget)
12742 			setupCustomTextEditing();
12743 	}
12744 
12745 	private bool wordWrapEnabled_;
12746 	void wordWrapEnabled(bool enabled) {
12747 		if(useCustomWidget) {
12748 			wordWrapEnabled_ = enabled;
12749 			version(use_new_text_system)
12750 				textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
12751 		} else version(win32_widgets) {
12752 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
12753 		}
12754 	}
12755 
12756 	override int minWidth() { return scaleWithDpi(16); }
12757 	override int widthStretchiness() { return 7; }
12758 	override int widthShrinkiness() { return 1; }
12759 
12760 	version(use_new_text_system)
12761 	override int maxHeight() {
12762 		if(useCustomWidget)
12763 			return tdh.maxHeight;
12764 		else
12765 			return super.maxHeight();
12766 	}
12767 
12768 	version(use_new_text_system)
12769 	override void focus() {
12770 		if(useCustomWidget && tdh)
12771 			tdh.focus();
12772 		else
12773 			super.focus();
12774 	}
12775 
12776 	void selectAll() {
12777 		if(useCustomWidget) {
12778 			version(use_new_text_system)
12779 				tdh.selectAll();
12780 			else version(trash_text)
12781 				textLayout.selectAll();
12782 			redraw();
12783 		} else version(win32_widgets) {
12784 			SendMessage(hwnd, EM_SETSEL, 0, -1);
12785 		}
12786 	}
12787 
12788 	version(use_new_text_system)
12789 		TextDisplayHelper tdh;
12790 
12791 	@property string content() {
12792 		if(useCustomWidget) {
12793 			version(use_new_text_system) {
12794 				return textLayout.getTextString();
12795 			} else version(trash_text) {
12796 				return textLayout.getPlainText();
12797 			}
12798 		} else version(win32_widgets) {
12799 			wchar[4096] bufferstack;
12800 			wchar[] buffer;
12801 			auto len = GetWindowTextLength(hwnd);
12802 			if(len < bufferstack.length)
12803 				buffer = bufferstack[0 .. len + 1];
12804 			else
12805 				buffer = new wchar[](len + 1);
12806 
12807 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
12808 			if(l >= 0)
12809 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
12810 			else
12811 				return null;
12812 		}
12813 
12814 		assert(0);
12815 	}
12816 	@property void content(string s) {
12817 		if(useCustomWidget) {
12818 			version(use_new_text_system) {
12819 				selectAll();
12820 				textLayout.selection.replaceContent(s);
12821 
12822 				tdh.adjustScrollbarSizes();
12823 				// these don't seem to help
12824 				// tdh.smw.setPosition(0, 0);
12825 				// tdh.scrollForCaret();
12826 
12827 				redraw();
12828 			} else version(trash_text) {
12829 				textLayout.clear();
12830 				textLayout.addText(s);
12831 
12832 				{
12833 				// FIXME: it should be able to get this info easier
12834 				auto painter = draw();
12835 				textLayout.redoLayout(painter);
12836 				}
12837 				auto cbb = textLayout.contentBoundingBox();
12838 				setContentSize(cbb.width, cbb.height);
12839 				/*
12840 				textLayout.addText(ForegroundColor.red, s);
12841 				textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/");
12842 				textLayout.addText(" is the best!");
12843 				*/
12844 				redraw();
12845 			}
12846 		} else version(win32_widgets) {
12847 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
12848 			SetWindowTextW(hwnd, bfr.ptr);
12849 		}
12850 	}
12851 
12852 	void addText(string txt) {
12853 		if(useCustomWidget) {
12854 			version(use_new_text_system) {
12855 				textLayout.appendText(txt);
12856 				tdh.adjustScrollbarSizes();
12857 				redraw();
12858 			} else if(trash_text) {
12859 				textLayout.addText(txt);
12860 
12861 				{
12862 				// FIXME: it should be able to get this info easier
12863 				auto painter = draw();
12864 				textLayout.redoLayout(painter);
12865 				}
12866 				auto cbb = textLayout.contentBoundingBox();
12867 				setContentSize(cbb.width, cbb.height);
12868 			}
12869 		} else version(win32_widgets) {
12870 			// get the current selection
12871 			DWORD StartPos, EndPos;
12872 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
12873 
12874 			// move the caret to the end of the text
12875 			int outLength = GetWindowTextLengthW(hwnd);
12876 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
12877 
12878 			// insert the text at the new caret position
12879 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
12880 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
12881 
12882 			// restore the previous selection
12883 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
12884 		}
12885 	}
12886 
12887 	version(custom_widgets)
12888 	version(trash_text)
12889 	override void paintFrameAndBackground(WidgetPainter painter) {
12890 		this.draw3dFrame(painter, FrameStyle.sunk, Color.white);
12891 	}
12892 
12893 	version(use_new_text_system)
12894 	TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
12895 		return new TextDisplayHelper(textLayout, smw);
12896 	}
12897 
12898 	version(use_new_text_system)
12899 	TextStyle defaultTextStyle() {
12900 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
12901 	}
12902 
12903 	version(use_new_text_system)
12904 	private OperatingSystemFont getUsedFont() {
12905 		auto cs = getComputedStyle();
12906 		auto font = cs.font;
12907 		if(font is null) {
12908 			font = new OperatingSystemFont;
12909 			font.loadDefault();
12910 		}
12911 		return font;
12912 	}
12913 
12914 	version(use_new_text_system) {
12915 		TextLayouter textLayout;
12916 
12917 		void setupCustomTextEditing() {
12918 			textLayout = new TextLayouter(defaultTextStyle());
12919 
12920 			auto smw = new ScrollMessageWidget(this);
12921 			if(!showingHorizontalScroll)
12922 				smw.horizontalScrollBar.hide();
12923 			if(!showingVerticalScroll)
12924 				smw.verticalScrollBar.hide();
12925 			this.tabStop = false;
12926 			smw.tabStop = false;
12927 			tdh = textDisplayHelperFactory(textLayout, smw);
12928 		}
12929 
12930 		override void newParentWindow(Window old, Window n) {
12931 			if(n is null) return;
12932 			this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
12933 				if(textLayout) {
12934 					if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
12935 						// the dpi change can change the font, so this informs the layouter that it has changed too
12936 						style.font_ = getUsedFont();
12937 
12938 						// arsd.core.writeln(this.parentWindow.win.actualDpi);
12939 					}
12940 				}
12941 			});
12942 		}
12943 
12944 	} else version(trash_text) {
12945 		static if(SimpledisplayTimerAvailable)
12946 			Timer caretTimer;
12947 		etc.TextLayout textLayout;
12948 
12949 		void setupCustomTextEditing() {
12950 			textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4));
12951 			textLayout.selectionXorColor = getComputedStyle().activeListXorColor;
12952 		}
12953 
12954 		override void paint(WidgetPainter painter) {
12955 			if(parentWindow.win.closed) return;
12956 
12957 			textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4);
12958 
12959 			/*
12960 			painter.outlineColor = Color.white;
12961 			painter.fillColor = Color.white;
12962 			painter.drawRectangle(Point(4, 4), contentWidth, contentHeight);
12963 			*/
12964 
12965 			painter.outlineColor = Color.black;
12966 			// painter.drawText(Point(4, 4), content, Point(width - 4, height - 4));
12967 
12968 			textLayout.caretShowingOnScreen = false;
12969 
12970 			textLayout.drawInto(painter, !parentWindow.win.closed && isFocused());
12971 		}
12972 	}
12973 
12974 	static class Style : Widget.Style {
12975 		override WidgetBackground background() {
12976 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
12977 		}
12978 
12979 		override Color foregroundColor() {
12980 			return WidgetPainter.visualTheme.foregroundColor;
12981 		}
12982 
12983 		override FrameStyle borderStyle() {
12984 			return FrameStyle.sunk;
12985 		}
12986 
12987 		override MouseCursor cursor() {
12988 			return GenericCursor.Text;
12989 		}
12990 	}
12991 	mixin OverrideStyle!Style;
12992 
12993 	version(trash_text)
12994 	version(custom_widgets)
12995 	override void defaultEventHandler_mousedown(MouseDownEvent ev) {
12996 		super.defaultEventHandler_mousedown(ev);
12997 		if(parentWindow.win.closed) return;
12998 		if(ev.button == MouseButton.left) {
12999 			if(textLayout.selectNone())
13000 				redraw();
13001 			textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY);
13002 			this.focus();
13003 			//this.parentWindow.win.grabInput();
13004 		} else if(ev.button == MouseButton.middle) {
13005 			static if(UsingSimpledisplayX11) {
13006 				getPrimarySelection(parentWindow.win, (in char[] txt) {
13007 					textLayout.insert(txt);
13008 					redraw();
13009 
13010 					auto cbb = textLayout.contentBoundingBox();
13011 					setContentSize(cbb.width, cbb.height);
13012 				});
13013 			}
13014 		}
13015 	}
13016 
13017 	version(trash_text)
13018 	version(custom_widgets)
13019 	override void defaultEventHandler_mouseup(MouseUpEvent ev) {
13020 		//this.parentWindow.win.releaseInputGrab();
13021 		super.defaultEventHandler_mouseup(ev);
13022 	}
13023 
13024 	version(trash_text)
13025 	version(custom_widgets)
13026 	override void defaultEventHandler_mousemove(MouseMoveEvent ev) {
13027 		super.defaultEventHandler_mousemove(ev);
13028 		if(ev.state & ModifierState.leftButtonDown) {
13029 			textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY);
13030 			redraw();
13031 		}
13032 	}
13033 
13034 	version(trash_text)
13035 	version(custom_widgets)
13036 	override void defaultEventHandler_focus(Event ev) {
13037 		super.defaultEventHandler_focus(ev);
13038 		if(parentWindow.win.closed) return;
13039 		auto painter = this.draw();
13040 		textLayout.drawCaret(painter);
13041 
13042 		static if(SimpledisplayTimerAvailable)
13043 		if(caretTimer) {
13044 			caretTimer.destroy();
13045 			caretTimer = null;
13046 		}
13047 
13048 		bool blinkingCaret = true;
13049 		static if(UsingSimpledisplayX11)
13050 			if(!Image.impl.xshmAvailable)
13051 				blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink
13052 
13053 		if(blinkingCaret)
13054 		static if(SimpledisplayTimerAvailable)
13055 		caretTimer = new Timer(500, {
13056 			if(parentWindow.win.closed) {
13057 				caretTimer.destroy();
13058 				return;
13059 			}
13060 			if(isFocused()) {
13061 				auto painter = this.draw();
13062 				textLayout.drawCaret(painter);
13063 			} else if(textLayout.caretShowingOnScreen) {
13064 				auto painter = this.draw();
13065 				textLayout.eraseCaret(painter);
13066 			}
13067 		});
13068 	}
13069 
13070 	version(trash_text) {
13071 		private string lastContentBlur;
13072 
13073 		override void defaultEventHandler_blur(Event ev) {
13074 			super.defaultEventHandler_blur(ev);
13075 			if(parentWindow.win.closed) return;
13076 			version(custom_widgets) {
13077 				auto painter = this.draw();
13078 				textLayout.eraseCaret(painter);
13079 				static if(SimpledisplayTimerAvailable)
13080 				if(caretTimer) {
13081 					caretTimer.destroy();
13082 					caretTimer = null;
13083 				}
13084 			}
13085 
13086 			if(this.content != lastContentBlur) {
13087 				auto evt = new ChangeEvent!string(this, &this.content);
13088 				evt.dispatch();
13089 				lastContentBlur = this.content;
13090 			}
13091 		}
13092 	}
13093 
13094 	version(win32_widgets) {
13095 		private string lastContentBlur;
13096 
13097 		override void defaultEventHandler_blur(Event ev) {
13098 			super.defaultEventHandler_blur(ev);
13099 
13100 			if(!useCustomWidget)
13101 			if(this.content != lastContentBlur) {
13102 				auto evt = new ChangeEvent!string(this, &this.content);
13103 				evt.dispatch();
13104 				lastContentBlur = this.content;
13105 			}
13106 		}
13107 	}
13108 
13109 
13110 	version(trash_text)
13111 	version(custom_widgets)
13112 	override void defaultEventHandler_char(CharEvent ev) {
13113 		super.defaultEventHandler_char(ev);
13114 		textLayout.insert(ev.character);
13115 		redraw();
13116 
13117 		// FIXME: too inefficient
13118 		auto cbb = textLayout.contentBoundingBox();
13119 		setContentSize(cbb.width, cbb.height);
13120 	}
13121 	version(trash_text)
13122 	version(custom_widgets)
13123 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
13124 		//super.defaultEventHandler_keydown(ev);
13125 		switch(ev.key) {
13126 			case Key.Delete:
13127 				textLayout.delete_();
13128 				redraw();
13129 			break;
13130 			case Key.Left:
13131 				textLayout.moveLeft();
13132 				redraw();
13133 			break;
13134 			case Key.Right:
13135 				textLayout.moveRight();
13136 				redraw();
13137 			break;
13138 			case Key.Up:
13139 				textLayout.moveUp();
13140 				redraw();
13141 			break;
13142 			case Key.Down:
13143 				textLayout.moveDown();
13144 				redraw();
13145 			break;
13146 			case Key.Home:
13147 				textLayout.moveHome();
13148 				redraw();
13149 			break;
13150 			case Key.End:
13151 				textLayout.moveEnd();
13152 				redraw();
13153 			break;
13154 			case Key.PageUp:
13155 				foreach(i; 0 .. 32)
13156 				textLayout.moveUp();
13157 				redraw();
13158 			break;
13159 			case Key.PageDown:
13160 				foreach(i; 0 .. 32)
13161 				textLayout.moveDown();
13162 				redraw();
13163 			break;
13164 
13165 			default:
13166 				 {} // intentionally blank, let "char" handle it
13167 		}
13168 		/*
13169 		if(ev.key == Key.Backspace) {
13170 			textLayout.backspace();
13171 			redraw();
13172 		}
13173 		*/
13174 		ensureVisibleInScroll(textLayout.caretBoundingBox());
13175 	}
13176 
13177 	version(use_new_text_system) {
13178 		bool showingVerticalScroll() { return true; }
13179 		bool showingHorizontalScroll() { return true; }
13180 	}
13181 }
13182 
13183 ///
13184 class LineEdit : EditableTextWidget {
13185 	override bool showingVerticalScroll() { return false; }
13186 	override bool showingHorizontalScroll() { return false; }
13187 
13188 	override int flexBasisWidth() { return 250; }
13189 
13190 	///
13191 	this(Widget parent) {
13192 		super(parent);
13193 		version(win32_widgets) {
13194 			createWin32Window(this, "edit"w, "",
13195 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13196 		} else version(custom_widgets) {
13197 			version(trash_text) {
13198 				setupCustomTextEditing();
13199 				addEventListener(delegate(CharEvent ev) {
13200 					if(ev.character == '\n')
13201 						ev.preventDefault();
13202 				});
13203 			}
13204 		} else static assert(false);
13205 	}
13206 
13207 	private this(bool useCustomWidget, Widget parent) {
13208 		if(!useCustomWidget)
13209 			this(parent);
13210 		else
13211 			super(true, parent);
13212 	}
13213 
13214 	version(use_new_text_system)
13215 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13216 		auto tdh = new TextDisplayHelper(textLayout, smw);
13217 		tdh.singleLine = true;
13218 		return tdh;
13219 	}
13220 
13221 	version(win32_widgets) {
13222 		mixin Padding!q{0};
13223 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13224 		override int maxHeight() { return minHeight; }
13225 	}
13226 
13227 	/+
13228 	@property void passwordMode(bool p) {
13229 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
13230 	}
13231 	+/
13232 }
13233 
13234 /// ditto
13235 class CustomLineEdit : LineEdit {
13236 	this(Widget parent) {
13237 		super(true, parent);
13238 	}
13239 }
13240 
13241 /++
13242 	A [LineEdit] that displays `*` in place of the actual characters.
13243 
13244 	Alas, Windows requires the window to be created differently to use this style,
13245 	so it had to be a new class instead of a toggle on and off on an existing object.
13246 
13247 	FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now.
13248 
13249 	History:
13250 		Added January 24, 2021
13251 +/
13252 class PasswordEdit : EditableTextWidget {
13253 	override bool showingVerticalScroll() { return false; }
13254 	override bool showingHorizontalScroll() { return false; }
13255 
13256 	override int flexBasisWidth() { return 250; }
13257 
13258 	version(use_new_text_system)
13259 	override TextStyle defaultTextStyle() {
13260 		auto cs = getComputedStyle();
13261 
13262 		auto osf = new class OperatingSystemFont {
13263 			this() {
13264 				super(cs.font);
13265 			}
13266 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
13267 				int count = 0;
13268 				foreach(dchar ch; text)
13269 					count++;
13270 				return count * super.stringWidth("*", window);
13271 			}
13272 		};
13273 
13274 		return new TextDisplayHelper.MyTextStyle(osf);
13275 	}
13276 
13277 	version(use_new_text_system)
13278 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13279 		static class TDH : TextDisplayHelper {
13280 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
13281 				singleLine = true;
13282 				super(textLayout, smw);
13283 			}
13284 
13285 			override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
13286 				char[256] buffer = void;
13287 				int bufferLength = 0;
13288 				foreach(dchar ch; text)
13289 					buffer[bufferLength++] = '*';
13290 				painter.drawText(upperLeft, buffer[0..bufferLength]);
13291 			}
13292 		}
13293 
13294 		return new TDH(textLayout, smw);
13295 	}
13296 
13297 	///
13298 	this(Widget parent) {
13299 		super(parent);
13300 		version(win32_widgets) {
13301 			createWin32Window(this, "edit"w, "",
13302 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13303 		} else version(custom_widgets) {
13304 			version(trash_text) {
13305 				setupCustomTextEditing();
13306 
13307 				// should this be under trash text? i think so.
13308 				addEventListener(delegate(CharEvent ev) {
13309 					if(ev.character == '\n')
13310 						ev.preventDefault();
13311 				});
13312 			}
13313 		} else static assert(false);
13314 	}
13315 
13316 	private this(bool useCustomWidget, Widget parent) {
13317 		if(!useCustomWidget)
13318 			this(parent);
13319 		else
13320 			super(true, parent);
13321 	}
13322 
13323 	version(win32_widgets) {
13324 		mixin Padding!q{2};
13325 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13326 		override int maxHeight() { return minHeight; }
13327 	}
13328 }
13329 
13330 /// ditto
13331 class CustomPasswordEdit : PasswordEdit {
13332 	this(Widget parent) {
13333 		super(true, parent);
13334 	}
13335 }
13336 
13337 
13338 ///
13339 class TextEdit : EditableTextWidget {
13340 	///
13341 	this(Widget parent) {
13342 		super(parent);
13343 		version(win32_widgets) {
13344 			createWin32Window(this, "edit"w, "",
13345 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
13346 		} else version(custom_widgets) {
13347 			version(trash_text)
13348 			setupCustomTextEditing();
13349 		} else static assert(false);
13350 	}
13351 
13352 	private this(bool useCustomWidget, Widget parent) {
13353 		if(!useCustomWidget)
13354 			this(parent);
13355 		else
13356 			super(true, parent);
13357 	}
13358 
13359 	override int maxHeight() { return int.max; }
13360 	override int heightStretchiness() { return 7; }
13361 
13362 	override int flexBasisWidth() { return 250; }
13363 	override int flexBasisHeight() { return 25; }
13364 }
13365 
13366 /// ditto
13367 class CustomTextEdit : TextEdit {
13368 	this(Widget parent) {
13369 		super(true, parent);
13370 	}
13371 }
13372 
13373 /+
13374 /++
13375 
13376 +/
13377 version(none)
13378 class RichTextDisplay : Widget {
13379 	@property void content(string c) {}
13380 	void appendContent(string c) {}
13381 }
13382 +/
13383 
13384 /++
13385 	A read-only text display
13386 
13387 	History:
13388 		Added October 31, 2023 (dub v11.3)
13389 +/
13390 class TextDisplay : EditableTextWidget {
13391 	this(string text, Widget parent) {
13392 		super(parent);
13393 		this.content = text;
13394 	}
13395 
13396 	override int maxHeight() { return int.max; }
13397 	override int minHeight() { return 50; }
13398 	override int heightStretchiness() { return 7; }
13399 
13400 	override int flexBasisWidth() { return 250; }
13401 	override int flexBasisHeight() { return 50; }
13402 
13403 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13404 		return new MyTextDisplayHelper(textLayout, smw);
13405 	}
13406 
13407 	override void registerMovement() {
13408 		super.registerMovement();
13409 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
13410 	}
13411 
13412 	static class MyTextDisplayHelper : TextDisplayHelper {
13413 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
13414 			smw.verticalScrollBar.hide();
13415 			smw.horizontalScrollBar.hide();
13416 			super(textLayout, smw);
13417 			this.readonly = true;
13418 		}
13419 
13420 		override void registerMovement() {
13421 			super.registerMovement();
13422 
13423 			// FIXME: do the horizontal one too as needed and make sure that it does
13424 			// wordwrapping again
13425 			if(l.height + smw.horizontalScrollBar.height > this.height)
13426 				smw.verticalScrollBar.show();
13427 			else
13428 				smw.verticalScrollBar.hide();
13429 
13430 			l.wordWrapWidth = this.width;
13431 
13432 			smw.verticalScrollBar.setPosition = 0;
13433 		}
13434 	}
13435 
13436 	class Style : Widget.Style {
13437 		// just want the generic look for these
13438 	}
13439 
13440 	mixin OverrideStyle!Style;
13441 }
13442 
13443 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
13444 /++
13445 	A scrollable viewer for an array of widgets. The widgets inside a list item can be whatever you want, and you can have any number of total items you want because only the visible widgets need to actually exist and load their data at a time, giving constantly predictable performance.
13446 
13447 
13448 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
13449 
13450 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
13451 
13452 	Note that some state in reused widget objects may either be preserved or reset when the user isn't expecting it. It is your responsibility to handle this when you load an item (try to save it when it is unloaded, then set it when reloaded), but my recommendation would be to have minimal extra state. For example, avoid having a scrollable widget inside a list, since the scroll state might change as it goes out and into view. Instead, I'd suggest making the list be a loader for a details pane on the side.
13453 
13454 	History:
13455 		Added August 12, 2024 (dub v11.6)
13456 +/
13457 abstract class GenericListViewWidget : Widget {
13458 	/++
13459 
13460 	+/
13461 	this(Widget parent) {
13462 		super(parent);
13463 
13464 		smw = new ScrollMessageWidget(this);
13465 		smw.addDefaultKeyboardListeners();
13466 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
13467 
13468 		inner = new GenericListViewWidgetInner(this, smw);
13469 	}
13470 
13471 	private ScrollMessageWidget smw;
13472 	private GenericListViewWidgetInner inner;
13473 
13474 	/++
13475 
13476 	+/
13477 	abstract GenericListViewItem itemFactory(Widget parent);
13478 	// in device-dependent pixels
13479 	/++
13480 
13481 	+/
13482 	abstract Size itemSize(); // use 0 to indicate it can stretch?
13483 
13484 	enum LayoutMode {
13485 		rows,
13486 		columns,
13487 		gridRowsFirst,
13488 		gridColumnsFirst
13489 	}
13490 	LayoutMode layoutMode() {
13491 		return LayoutMode.rows;
13492 	}
13493 
13494 	private int itemCount_;
13495 
13496 	/++
13497 		Sets the count of available items in the list. This will not allocate any items, but it will adjust the scroll bars and try to load items up to this count on-demand as they appear visible.
13498 	+/
13499 	void setItemCount(int count) {
13500 		smw.setTotalArea(inner.width, count * itemSize().height);
13501 		smw.setViewableArea(inner.width, inner.height);
13502 		this.itemCount_ = count;
13503 	}
13504 
13505 	/++
13506 		Returns the current count of items expected to available in the list.
13507 	+/
13508 	int itemCount() {
13509 		return this.itemCount_;
13510 	}
13511 
13512 	/++
13513 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
13514 
13515 		Note you must $(I also) call [setItemCount] if the total item count has changed.
13516 	+/
13517 	void notifyItemsChanged(int index, int count = 1) {
13518 	}
13519 	/// ditto
13520 	void notifyItemsInserted(int index, int count = 1) {
13521 	}
13522 	/// ditto
13523 	void notifyItemsRemoved(int index, int count = 1) {
13524 	}
13525 	/// ditto
13526 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
13527 	}
13528 
13529 	private GenericListViewItem[] items;
13530 }
13531 
13532 /// ditto
13533 abstract class GenericListViewItem : Widget {
13534 	/++
13535 	+/
13536 	this(Widget parent) {
13537 		super(parent);
13538 	}
13539 
13540 	private int _currentIndex = -1;
13541 
13542 	private void showItemPrivate(int idx) {
13543 		showItem(idx);
13544 		_currentIndex = idx;
13545 	}
13546 
13547 	/++
13548 		Implement this to show an item from your data backing to the list.
13549 
13550 		Note that even if you are showing the requested index already, you should still try to reload it because it is possible the index now points to a different item (e.g. an item was added so all the indexes have changed) or if data has changed in this index and it is requesting you to update it prior to a repaint.
13551 	+/
13552 	abstract void showItem(int idx);
13553 
13554 	/++
13555 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
13556 
13557 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
13558 
13559 		Inside the call to `showItem`, `currentIndexLoaded` is the old index, and the argument to `showItem` is the new index. You might use that to save state to the right place as needed before you overwrite it with the new item.
13560 	+/
13561 	final int currentIndexLoaded() {
13562 		return _currentIndex;
13563 	}
13564 }
13565 
13566 ///
13567 unittest {
13568 	import arsd.minigui;
13569 
13570 	import std.conv;
13571 
13572 	void main() {
13573 		auto mw = new MainWindow();
13574 
13575 		static class MyListViewItem : GenericListViewItem {
13576 			this(Widget parent) {
13577 				super(parent);
13578 
13579 				label = new TextLabel("unloaded", TextAlignment.Left, this);
13580 				button = new Button("Click", this);
13581 
13582 				button.addEventListener("triggered", (){
13583 					messageBox(text("clicked ", currentIndexLoaded()));
13584 				});
13585 			}
13586 			override void showItem(int idx) {
13587 				label.label = "Item " ~ to!string(idx);
13588 			}
13589 
13590 			TextLabel label;
13591 			Button button;
13592 		}
13593 
13594 		auto widget = new class GenericListViewWidget {
13595 			this() {
13596 				super(mw);
13597 			}
13598 			override GenericListViewItem itemFactory(Widget parent) {
13599 				return new MyListViewItem(parent);
13600 			}
13601 			override Size itemSize() {
13602 				return Size(0, scaleWithDpi(80));
13603 			}
13604 		};
13605 
13606 		widget.setItemCount(5000);
13607 
13608 		mw.loop();
13609 	}
13610 }
13611 
13612 private class GenericListViewWidgetInner : Widget {
13613 	this(GenericListViewWidget glvw, ScrollMessageWidget smw) {
13614 		super(smw);
13615 		this.glvw = glvw;
13616 		this.tabStop = false;
13617 
13618 		reloadVisible();
13619 
13620 		smw.addEventListener("scroll", () {
13621 			reloadVisible();
13622 		});
13623 	}
13624 
13625 	override void registerMovement() {
13626 		super.registerMovement();
13627 		if(glvw && glvw.smw)
13628 			glvw.smw.setViewableArea(this.width, this.height);
13629 	}
13630 
13631 	void reloadVisible() {
13632 		auto y = glvw.smw.position.y / glvw.itemSize.height;
13633 		int offset = glvw.smw.position.y % glvw.itemSize.height;
13634 
13635 		if(offset || y >= glvw.itemCount())
13636 			y--;
13637 		if(y < 0)
13638 			y = 0;
13639 
13640 		recomputeChildLayout();
13641 
13642 		foreach(item; glvw.items) {
13643 			if(y < glvw.itemCount()) {
13644 				item.showItemPrivate(y);
13645 				item.show();
13646 			} else {
13647 				item.hide();
13648 			}
13649 			y++;
13650 		}
13651 
13652 		this.redraw();
13653 	}
13654 
13655 	private GenericListViewWidget glvw;
13656 
13657 	private bool inRcl;
13658 	override void recomputeChildLayout() {
13659 		if(inRcl)
13660 			return;
13661 		inRcl = true;
13662 		scope(exit)
13663 			inRcl = false;
13664 
13665 		auto ih = glvw.itemSize().height;
13666 
13667 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
13668 		bool hadNew;
13669 		while(glvw.items.length < itemCount) {
13670 			// FIXME: free the old items? maybe just set length
13671 			glvw.items ~= glvw.itemFactory(this);
13672 			hadNew = true;
13673 		}
13674 
13675 		if(hadNew)
13676 			reloadVisible();
13677 
13678 		int y = -(glvw.smw.position.y % ih);
13679 		foreach(child; children) {
13680 			child.x = 0;
13681 			child.y = y;
13682 			y += glvw.itemSize().height;
13683 			child.width = this.width;
13684 			child.height = ih;
13685 
13686 			child.recomputeChildLayout();
13687 		}
13688 	}
13689 }
13690 
13691 
13692 
13693 ///
13694 class MessageBox : Window {
13695 	private string message;
13696 	MessageBoxButton buttonPressed = MessageBoxButton.None;
13697 	///
13698 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
13699 		super(300, 100);
13700 
13701 		assert(buttons.length);
13702 		assert(buttons.length ==  buttonIds.length);
13703 
13704 		this.message = message;
13705 
13706 		auto label = new TextDisplay(message, this);
13707 
13708 		auto hl = new HorizontalLayout(this);
13709 		auto spacer = new HorizontalSpacer(hl); // to right align
13710 
13711 		foreach(idx, buttonText; buttons) {
13712 			auto button = new CommandButton(buttonText, hl);
13713 
13714 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
13715 				this.buttonPressed = buttonIds[idx];
13716 				win.close();
13717 			}; })(idx));
13718 
13719 			if(idx == 0)
13720 				button.focus();
13721 		}
13722 
13723 		if(buttons.length == 1)
13724 			auto spacer2 = new HorizontalSpacer(hl); // to center it
13725 
13726 		win.resize(scaleWithDpi(300), this.minHeight());
13727 
13728 		win.show();
13729 		redraw();
13730 	}
13731 
13732 	mixin Padding!q{16};
13733 }
13734 
13735 ///
13736 enum MessageBoxStyle {
13737 	OK, ///
13738 	OKCancel, ///
13739 	RetryCancel, ///
13740 	YesNo, ///
13741 	YesNoCancel, ///
13742 	RetryCancelContinue /// In a multi-part process, if one part fails, ask the user if you should retry that failed step, cancel the entire process, or just continue with the next step, accepting failure on this step.
13743 }
13744 
13745 ///
13746 enum MessageBoxIcon {
13747 	None, ///
13748 	Info, ///
13749 	Warning, ///
13750 	Error ///
13751 }
13752 
13753 /// Identifies the button the user pressed on a message box.
13754 enum MessageBoxButton {
13755 	None, /// The user closed the message box without clicking any of the buttons.
13756 	OK, ///
13757 	Cancel, ///
13758 	Retry, ///
13759 	Yes, ///
13760 	No, ///
13761 	Continue ///
13762 }
13763 
13764 
13765 /++
13766 	Displays a modal message box, blocking until the user dismisses it.
13767 
13768 	Returns: the button pressed.
13769 +/
13770 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13771 	version(win32_widgets) {
13772 		WCharzBuffer t = WCharzBuffer(title);
13773 		WCharzBuffer m = WCharzBuffer(message);
13774 		UINT type;
13775 		with(MessageBoxStyle)
13776 		final switch(style) {
13777 			case OK: type |= MB_OK; break;
13778 			case OKCancel: type |= MB_OKCANCEL; break;
13779 			case RetryCancel: type |= MB_RETRYCANCEL; break;
13780 			case YesNo: type |= MB_YESNO; break;
13781 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
13782 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
13783 		}
13784 		with(MessageBoxIcon)
13785 		final switch(icon) {
13786 			case None: break;
13787 			case Info: type |= MB_ICONINFORMATION; break;
13788 			case Warning: type |= MB_ICONWARNING; break;
13789 			case Error: type |= MB_ICONERROR; break;
13790 		}
13791 		switch(MessageBoxW(null, m.ptr, t.ptr, type)) {
13792 			case IDOK: return MessageBoxButton.OK;
13793 			case IDCANCEL: return MessageBoxButton.Cancel;
13794 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
13795 			case IDYES: return MessageBoxButton.Yes;
13796 			case IDNO: return MessageBoxButton.No;
13797 			case IDCONTINUE: return MessageBoxButton.Continue;
13798 			default: return MessageBoxButton.None;
13799 		}
13800 	} else {
13801 		string[] buttons;
13802 		MessageBoxButton[] buttonIds;
13803 		with(MessageBoxStyle)
13804 		final switch(style) {
13805 			case OK:
13806 				buttons = ["OK"];
13807 				buttonIds = [MessageBoxButton.OK];
13808 			break;
13809 			case OKCancel:
13810 				buttons = ["OK", "Cancel"];
13811 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
13812 			break;
13813 			case RetryCancel:
13814 				buttons = ["Retry", "Cancel"];
13815 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
13816 			break;
13817 			case YesNo:
13818 				buttons = ["Yes", "No"];
13819 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
13820 			break;
13821 			case YesNoCancel:
13822 				buttons = ["Yes", "No", "Cancel"];
13823 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
13824 			break;
13825 			case RetryCancelContinue:
13826 				buttons = ["Try Again", "Cancel", "Continue"];
13827 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
13828 			break;
13829 		}
13830 		auto mb = new MessageBox(message, buttons, buttonIds);
13831 		EventLoop el = EventLoop.get;
13832 		el.run(() { return !mb.win.closed; });
13833 		return mb.buttonPressed;
13834 	}
13835 }
13836 
13837 /// ditto
13838 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13839 	return messageBox(null, message, style, icon);
13840 }
13841 
13842 
13843 
13844 ///
13845 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
13846 
13847 /++
13848 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
13849 
13850 	History:
13851 		The data members were `public` (albiet undocumented and not intended for use) prior to May 13, 2021. They are now `private`, reflecting the single intended use of this object.
13852 +/
13853 struct EventListener {
13854 	private Widget widget;
13855 	private string event;
13856 	private EventHandler handler;
13857 	private bool useCapture;
13858 
13859 	///
13860 	void disconnect() {
13861 		widget.removeEventListener(this);
13862 	}
13863 }
13864 
13865 /++
13866 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
13867 
13868 	Now, I recommend you use a statically typed event object instead.
13869 
13870 	See_Also: [Event]
13871 +/
13872 enum EventType : string {
13873 	click = "click", ///
13874 
13875 	mouseenter = "mouseenter", ///
13876 	mouseleave = "mouseleave", ///
13877 	mousein = "mousein", ///
13878 	mouseout = "mouseout", ///
13879 	mouseup = "mouseup", ///
13880 	mousedown = "mousedown", ///
13881 	mousemove = "mousemove", ///
13882 
13883 	keydown = "keydown", ///
13884 	keyup = "keyup", ///
13885 	char_ = "char", ///
13886 
13887 	focus = "focus", ///
13888 	blur = "blur", ///
13889 
13890 	triggered = "triggered", ///
13891 
13892 	change = "change", ///
13893 }
13894 
13895 /++
13896 	Represents an event that is currently being processed.
13897 
13898 
13899 	Minigui's event model is based on the web browser. An event has a name, a target,
13900 	and an associated data object. It starts from the window and works its way down through
13901 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
13902 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
13903 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
13904 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
13905 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
13906 	whenever propagation is done, not only if it gets to the end of the chain).
13907 
13908 	This model has several nice points:
13909 
13910 	$(LIST
13911 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
13912 		  with event handlers set, then add/remove children as much as you want without needing
13913 		  to manage the event handlers on them - the parent alone can manage everything.
13914 
13915 		* It is easy to create new custom events in your application.
13916 
13917 		* It is familiar to many web developers.
13918 	)
13919 
13920 	There's a few downsides though:
13921 
13922 	$(LIST
13923 		* There's not a lot of type safety.
13924 
13925 		* You don't get a static list of what events a widget can emit.
13926 
13927 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
13928 		  the central delegation benefit is it can be lead to debugging of action at a distance.
13929 	)
13930 
13931 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
13932 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
13933 	to simply use a D object type which provides a static interface as well as a built-in event name.
13934 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
13935 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
13936 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
13937 	to having a little more help from the D compiler and documentation generator.
13938 
13939 	Your code would change like this:
13940 
13941 	---
13942 	// old
13943 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
13944 
13945 	// new
13946 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
13947 	---
13948 
13949 	The old-style code will still work, but using certain members of the [Event] class will generate deprecation warnings. Changing handlers to the new style will silence all those warnings at once without requiring any other changes to your code.
13950 
13951 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
13952 
13953 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
13954 
13955 	Thus the family of functions are:
13956 
13957 	[Widget.addEventListener] is the fully-flexible base method. It has two main overload families: one with the string and one without. The one with the string takes the Event object, the one without determines the string from the type you pass. The string "*" matches ALL events that pass through.
13958 
13959 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
13960 
13961 	[Widget.setDefaultEventHandler] is what is called if no preventDefault was called. This should be called in the widget's constructor to set default behaivor. Default event handlers are only called on the event target.
13962 
13963 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
13964 
13965 	---
13966 	class MyCheckbox : Widget {
13967 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
13968 		/// It is NOT actually required but should be used whenever possible.
13969 		mixin Emits!(ChangeEvent!bool);
13970 
13971 		this(Widget parent) {
13972 			super(parent);
13973 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
13974 		}
13975 
13976 		private bool _checked;
13977 		@property bool checked() { return _checked; }
13978 		@property void checked(bool set) {
13979 			_checked = set;
13980 			emit!(ChangeEvent!bool)(&checked);
13981 		}
13982 	}
13983 	---
13984 
13985 	## Creating Your Own Events
13986 
13987 	To avoid clashing in the string namespace, your events should use your module and class name as the event string. The simple code `mixin Register;` in your Event subclass will do this for you.
13988 
13989 	---
13990 	class MyEvent : Event {
13991 		this(Widget target) { super(EventString, target); }
13992 		mixin Register; // adds EventString and other reflection information
13993 	}
13994 	---
13995 
13996 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
13997 
13998 	History:
13999 		Prior to May 2021, Event had a set of pre-made members with no extensibility (outside of diy casts) and no static checks on field presence.
14000 
14001 		After that, those old pre-made members are deprecated accessors and the fields are moved to child classes. To transition, change string events to typed events or do a dynamic cast (don't forget the null check!) in your handler.
14002 +/
14003 /+
14004 
14005 	## General Conventions
14006 
14007 	Change events should NOT be emitted when a value is changed programmatically. Indeed, methods should usually not send events. The point of an event is to know something changed and when you call a method, you already know about it.
14008 
14009 
14010 	## Qt-style signals and slots
14011 
14012 	Some events make sense to use with just name and data type. These are one-way notifications with no propagation nor default behavior and thus separate from the other event system.
14013 
14014 	The intention is for events to be used when
14015 
14016 	---
14017 	class Demo : Widget {
14018 		this() {
14019 			myPropertyChanged = Signal!int(this);
14020 		}
14021 		@property myProperty(int v) {
14022 			myPropertyChanged.emit(v);
14023 		}
14024 
14025 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
14026 		// but it can just genuinely not care about `this` since that's not really passed.
14027 	}
14028 
14029 	class Foo : Widget {
14030 		// the slot uda is not necessary, but it helps the script and ui builder find it.
14031 		@slot void setValue(int v) { ... }
14032 	}
14033 
14034 	demo.myPropertyChanged.connect(&foo.setValue);
14035 	---
14036 
14037 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
14038 
14039 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
14040 
14041 	class StringChangeEvent : ChangeEvent, Signal!string {
14042 		mixin SignalImpl
14043 	}
14044 
14045 +/
14046 class Event : ReflectableProperties {
14047 	/// Creates an event without populating any members and without sending it. See [dispatch]
14048 	this(string eventName, Widget emittedBy) {
14049 		this.eventName = eventName;
14050 		this.srcElement = emittedBy;
14051 	}
14052 
14053 
14054 	/// Implementations for the [ReflectableProperties] interface/
14055 	void getPropertiesList(scope void delegate(string name) sink) const {}
14056 	/// ditto
14057 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
14058 	/// ditto
14059 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
14060 		return SetPropertyResult.notPermitted;
14061 	}
14062 
14063 
14064 	/+
14065 	/++
14066 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
14067 
14068 		It is just protected so the mixin template can see it from user modules. If I made it private, even my own mixin template couldn't see it due to mixin scoping rules.
14069 	+/
14070 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
14071 		if(value.length == 0) {
14072 			finalSink(memberName, `""`);
14073 			return;
14074 		}
14075 
14076 		char[1024] bufferBacking;
14077 		char[] buffer = bufferBacking;
14078 		int bufferPosition;
14079 
14080 		void sink(char ch) {
14081 			if(bufferPosition >= buffer.length)
14082 				buffer.length = buffer.length + 1024;
14083 			buffer[bufferPosition++] = ch;
14084 		}
14085 
14086 		sink('"');
14087 
14088 		foreach(ch; value) {
14089 			switch(ch) {
14090 				case '\\':
14091 					sink('\\'); sink('\\');
14092 				break;
14093 				case '"':
14094 					sink('\\'); sink('"');
14095 				break;
14096 				case '\n':
14097 					sink('\\'); sink('n');
14098 				break;
14099 				case '\r':
14100 					sink('\\'); sink('r');
14101 				break;
14102 				case '\t':
14103 					sink('\\'); sink('t');
14104 				break;
14105 				default:
14106 					sink(ch);
14107 			}
14108 		}
14109 
14110 		sink('"');
14111 
14112 		finalSink(memberName, buffer[0 .. bufferPosition]);
14113 	}
14114 	+/
14115 
14116 	/+
14117 	enum EventInitiator {
14118 		system,
14119 		minigui,
14120 		user
14121 	}
14122 
14123 	immutable EventInitiator; initiatedBy;
14124 	+/
14125 
14126 	/++
14127 		Events should generally follow the propagation model, but there's some exceptions
14128 		to that rule. If so, they should override this to return false. In that case, only
14129 		bubbling event handlers on the target itself and capturing event handlers on the containing
14130 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
14131 		capture -> target -> bubble process.)
14132 
14133 		History:
14134 			Added May 12, 2021
14135 	+/
14136 	bool propagates() const pure nothrow @nogc @safe {
14137 		return true;
14138 	}
14139 
14140 	/++
14141 		hints as to whether preventDefault will actually do anything. not entirely reliable.
14142 
14143 		History:
14144 			Added May 14, 2021
14145 	+/
14146 	bool cancelable() const pure nothrow @nogc @safe {
14147 		return true;
14148 	}
14149 
14150 	/++
14151 		You can mix this into child class to register some boilerplate. It includes the `EventString`
14152 		member, a constructor, and implementations of the dynamic get data interfaces.
14153 
14154 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
14155 
14156 
14157 		You can override the default EventString by simply providing your own in the form of
14158 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
14159 		which provides some namespace protection against conflicts in other libraries while still being fairly
14160 		easy to use.
14161 
14162 		If you provide your own constructor, it will override the default constructor provided here. A constructor
14163 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
14164 		first argument to your constructor.
14165 
14166 		History:
14167 			Added May 13, 2021.
14168 	+/
14169 	protected static mixin template Register() {
14170 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
14171 		this(Widget target) { super(EventString, target); }
14172 
14173 		mixin ReflectableProperties.RegisterGetters;
14174 	}
14175 
14176 	/++
14177 		This is the widget that emitted the event.
14178 
14179 
14180 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
14181 
14182 		History:
14183 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
14184 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
14185 			so I don't intend to remove these aliases.
14186 	+/
14187 	Widget source;
14188 	/// ditto
14189 	alias source target;
14190 	/// ditto
14191 	alias source srcElement;
14192 
14193 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
14194 
14195 	/// Prevents the default event handler (if there is one) from being called
14196 	void preventDefault() {
14197 		lastDefaultPrevented = true;
14198 		defaultPrevented = true;
14199 	}
14200 
14201 	/// Stops the event propagation immediately.
14202 	void stopPropagation() {
14203 		propagationStopped = true;
14204 	}
14205 
14206 	private bool defaultPrevented;
14207 	private bool propagationStopped;
14208 	private string eventName;
14209 
14210 	private bool isBubbling;
14211 
14212 	/// This is an internal implementation detail you should not use. It would be private if the language allowed it and it may be removed without notice.
14213 	protected void adjustScrolling() { }
14214 	/// ditto
14215 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
14216 
14217 	/++
14218 		this sends it only to the target. If you want propagation, use dispatch() instead.
14219 
14220 		This should be made private!!!
14221 
14222 	+/
14223 	void sendDirectly() {
14224 		if(srcElement is null)
14225 			return;
14226 
14227 		// i capturing on the parent too. The main reason for this is that gives a central place to log all events for the debug window.
14228 
14229 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
14230 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
14231 
14232 		adjustScrolling();
14233 
14234 		if(auto e = target.parentWindow) {
14235 			if(auto handlers = "*" in e.capturingEventHandlers)
14236 			foreach(handler; *handlers)
14237 				if(handler) handler(e, this);
14238 			if(auto handlers = eventName in e.capturingEventHandlers)
14239 			foreach(handler; *handlers)
14240 				if(handler) handler(e, this);
14241 		}
14242 
14243 		auto e = srcElement;
14244 
14245 		if(auto handlers = eventName in e.bubblingEventHandlers)
14246 		foreach(handler; *handlers)
14247 			if(handler) handler(e, this);
14248 
14249 		if(auto handlers = "*" in e.bubblingEventHandlers)
14250 		foreach(handler; *handlers)
14251 			if(handler) handler(e, this);
14252 
14253 		// there's never a default for a catch-all event
14254 		if(!defaultPrevented)
14255 			if(eventName in e.defaultEventHandlers)
14256 				e.defaultEventHandlers[eventName](e, this);
14257 	}
14258 
14259 	/// this dispatches the element using the capture -> target -> bubble process
14260 	void dispatch() {
14261 		if(srcElement is null)
14262 			return;
14263 
14264 		if(!propagates) {
14265 			sendDirectly;
14266 			return;
14267 		}
14268 
14269 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
14270 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
14271 
14272 		adjustScrolling();
14273 		// first capture, then bubble
14274 
14275 		Widget[] chain;
14276 		Widget curr = srcElement;
14277 		while(curr) {
14278 			auto l = curr;
14279 			chain ~= l;
14280 			curr = curr.parent;
14281 		}
14282 
14283 		isBubbling = false;
14284 
14285 		foreach_reverse(e; chain) {
14286 			if(auto handlers = "*" in e.capturingEventHandlers)
14287 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14288 
14289 			if(propagationStopped)
14290 				break;
14291 
14292 			if(auto handlers = eventName in e.capturingEventHandlers)
14293 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14294 
14295 			// the default on capture should really be to always do nothing
14296 
14297 			//if(!defaultPrevented)
14298 			//	if(eventName in e.defaultEventHandlers)
14299 			//		e.defaultEventHandlers[eventName](e.element, this);
14300 
14301 			if(propagationStopped)
14302 				break;
14303 		}
14304 
14305 		int adjustX;
14306 		int adjustY;
14307 
14308 		isBubbling = true;
14309 		if(!propagationStopped)
14310 		foreach(e; chain) {
14311 			if(auto handlers = eventName in e.bubblingEventHandlers)
14312 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14313 
14314 			if(propagationStopped)
14315 				break;
14316 
14317 			if(auto handlers = "*" in e.bubblingEventHandlers)
14318 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14319 
14320 			if(propagationStopped)
14321 				break;
14322 
14323 			if(e.encapsulatedChildren()) {
14324 				adjustClientCoordinates(adjustX, adjustY);
14325 				target = e;
14326 			} else {
14327 				adjustX += e.x;
14328 				adjustY += e.y;
14329 			}
14330 		}
14331 
14332 		if(!defaultPrevented)
14333 		foreach(e; chain) {
14334 			if(eventName in e.defaultEventHandlers)
14335 				e.defaultEventHandlers[eventName](e, this);
14336 		}
14337 	}
14338 
14339 
14340 	/* old compatibility things */
14341 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
14342 	final @property {
14343 		Key key() { return (cast(KeyEventBase) this).key; }
14344 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
14345 
14346 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
14347 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
14348 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
14349 	}
14350 
14351 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
14352 	final @property {
14353 		int clientX() { return (cast(MouseEventBase) this).clientX; }
14354 		int clientY() { return (cast(MouseEventBase) this).clientY; }
14355 
14356 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
14357 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
14358 
14359 		int button() { return (cast(MouseEventBase) this).button; }
14360 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
14361 	}
14362 
14363 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
14364 	final @property {
14365 		int state() {
14366 			if(auto meb = cast(MouseEventBase) this)
14367 				return meb.state;
14368 			if(auto keb = cast(KeyEventBase) this)
14369 				return keb.state;
14370 			assert(0);
14371 		}
14372 	}
14373 
14374 	deprecated("Use a CharEvent instead of Event in your handler going forward")
14375 	final @property {
14376 		dchar character() {
14377 			if(auto ce = cast(CharEvent) this)
14378 				return ce.character;
14379 			return dchar.init;
14380 		}
14381 	}
14382 
14383 	// for change events
14384 	@property {
14385 		///
14386 		int intValue() { return 0; }
14387 		///
14388 		string stringValue() { return null; }
14389 	}
14390 }
14391 
14392 /++
14393 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
14394 
14395 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
14396 	dynamic and custom events, but the static list helps ensure you get them right.
14397 
14398 	If this is declared, you can use [Widget.emit] to send the event.
14399 
14400 	All events work the same way though, following the capture->widget->bubble model described under [Event].
14401 
14402 	History:
14403 		Added May 4, 2021
14404 +/
14405 mixin template Emits(EventType) {
14406 	import arsd.minigui : EventString;
14407 	static if(is(EventType : Event) && !is(EventType == Event))
14408 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
14409 	else
14410 		static assert(0, "You can only emit subclasses of Event");
14411 }
14412 
14413 /// ditto
14414 mixin template Emits(string eventString) {
14415 	mixin("private Event[0] emits_" ~ eventString ~";");
14416 }
14417 
14418 /*
14419 class SignalEvent(string name) : Event {
14420 
14421 }
14422 */
14423 
14424 /++
14425 	Command Events are used with a widget wants to issue a higher-level, yet loosely coupled command do its parents and other interested listeners, for example, "scroll up".
14426 
14427 
14428 	Command Events are a bit special in the way they're used. You don't typically refer to them by object, but instead by a name string and a set of arguments. The expectation is that they will be delegated to a parent, which "consumes" the command - it handles it and stops its propagation upward. The [consumesCommand] method will call your handler with the arguments, then stop the command event's propagation for you, meaning you don't have to call [Event.stopPropagation]. A command event should have no default behavior, so calling [Event.preventDefault] is not necessary either.
14429 
14430 	History:
14431 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
14432 +/
14433 class CommandEvent : Event {
14434 	enum EventString = "command";
14435 	this(Widget source, string CommandString = EventString) {
14436 		super(CommandString, source);
14437 	}
14438 }
14439 
14440 /++
14441 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
14442 +/
14443 class CommandEventWithArgs(Args...) : CommandEvent {
14444 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
14445 	Args args;
14446 }
14447 
14448 /++
14449 	Declares that the given widget consumes a command identified by the `CommandString` AND containing `Args`. Your `handler` is called with the arguments, then the event's propagation is stopped, so it will not be seen by the consumer's parents.
14450 
14451 	See [CommandEvent] for more information.
14452 
14453 	Returns:
14454 		The [EventListener] you can use to remove the handler.
14455 +/
14456 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
14457 	return w.addEventListener(CommandString, (Event ev) {
14458 		if(ev.target is w)
14459 			return; // it does not consume its own commands!
14460 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
14461 			handler(cev.args);
14462 			ev.stopPropagation();
14463 		}
14464 	});
14465 }
14466 
14467 /++
14468 	Emits a command to the sender widget's parents with the given `CommandString` and `args`. You have no way of knowing if it was ever actually consumed due to the loose coupling. Instead, the consumer may broadcast a state update back toward you.
14469 +/
14470 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
14471 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
14472 	event.dispatch();
14473 }
14474 
14475 class ResizeEvent : Event {
14476 	enum EventString = "resize";
14477 
14478 	this(Widget target) { super(EventString, target); }
14479 
14480 	override bool propagates() const { return false; }
14481 }
14482 
14483 /++
14484 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
14485 
14486 	ClosedEvent happens when the window has been closed. It is already gone by the time this event fires, meaning you cannot prevent the close. Use [ClosingEvent] if you want to cancel, use [ClosedEvent] if you simply want to be notified.
14487 
14488 	History:
14489 		Added June 21, 2021 (dub v10.1)
14490 +/
14491 class ClosingEvent : Event {
14492 	enum EventString = "closing";
14493 
14494 	this(Widget target) { super(EventString, target); }
14495 
14496 	override bool propagates() const { return false; }
14497 	override bool cancelable() const { return true; }
14498 }
14499 
14500 /// ditto
14501 class ClosedEvent : Event {
14502 	enum EventString = "closed";
14503 
14504 	this(Widget target) { super(EventString, target); }
14505 
14506 	override bool propagates() const { return false; }
14507 	override bool cancelable() const { return false; }
14508 }
14509 
14510 ///
14511 class BlurEvent : Event {
14512 	enum EventString = "blur";
14513 
14514 	// FIXME: related target?
14515 	this(Widget target) { super(EventString, target); }
14516 
14517 	override bool propagates() const { return false; }
14518 }
14519 
14520 ///
14521 class FocusEvent : Event {
14522 	enum EventString = "focus";
14523 
14524 	// FIXME: related target?
14525 	this(Widget target) { super(EventString, target); }
14526 
14527 	override bool propagates() const { return false; }
14528 }
14529 
14530 /++
14531 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
14532 
14533 	History:
14534 		Added July 3, 2021
14535 +/
14536 class FocusInEvent : Event {
14537 	enum EventString = "focusin";
14538 
14539 	// FIXME: related target?
14540 	this(Widget target) { super(EventString, target); }
14541 
14542 	override bool cancelable() const { return false; }
14543 }
14544 
14545 /// ditto
14546 class FocusOutEvent : Event {
14547 	enum EventString = "focusout";
14548 
14549 	// FIXME: related target?
14550 	this(Widget target) { super(EventString, target); }
14551 
14552 	override bool cancelable() const { return false; }
14553 }
14554 
14555 ///
14556 class ScrollEvent : Event {
14557 	enum EventString = "scroll";
14558 	this(Widget target) { super(EventString, target); }
14559 
14560 	override bool cancelable() const { return false; }
14561 }
14562 
14563 /++
14564 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
14565 
14566 	History:
14567 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
14568 +/
14569 class CharEvent : Event {
14570 	enum EventString = "char";
14571 	this(Widget target, dchar ch) {
14572 		character = ch;
14573 		super(EventString, target);
14574 	}
14575 
14576 	immutable dchar character;
14577 }
14578 
14579 /++
14580 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
14581 +/
14582 abstract class ChangeEventBase : Event {
14583 	enum EventString = "change";
14584 	this(Widget target) {
14585 		super(EventString, target);
14586 	}
14587 
14588 	/+
14589 		// idk where or how exactly i want to do this.
14590 		// i might come back to it later.
14591 
14592 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
14593 	// this way the source doesn't get too confused (think of a nested scroll widget)
14594 	//
14595 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
14596 	// then you consume that command and change you scroll x position to whatever. then you do
14597 	// some kind of change event that is broadcast back to the children and any horizontal scroll
14598 	// listeners are now able to update, without having an explicit connection between them.
14599 	void broadcastToChildren(string fieldName) {
14600 
14601 	}
14602 	+/
14603 }
14604 
14605 /++
14606 	Single-value widgets (that is, ones with a programming interface that just expose a value that the user has control over) should emit this after their value changes.
14607 
14608 
14609 	Generally speaking, if your widget can reasonably have a `@property T value();` or `@property bool checked();` method, it should probably emit this event when that value changes to inform its parents that they can now read a new value. Whether you emit it on each keystroke or other intermediate values or only when a value is committed (e.g. when the user leaves the field) is up to the widget. You might even make that a togglable property depending on your needs (emitting events can get expensive).
14610 
14611 	The delegate you pass to the constructor ought to be a handle to your getter property. If your widget has `@property string value()` for example, you emit `ChangeEvent!string(&value);`
14612 
14613 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
14614 
14615 	History:
14616 		Added May 11, 2021. Prior to that, widgets would more likely just send `new Event("change")`. These typed ChangeEvents are still compatible with listeners subscribed to generic change events.
14617 +/
14618 class ChangeEvent(T) : ChangeEventBase {
14619 	this(Widget target, T delegate() getNewValue) {
14620 		assert(getNewValue !is null);
14621 		this.getNewValue = getNewValue;
14622 		super(target);
14623 	}
14624 
14625 	private T delegate() getNewValue;
14626 
14627 	/++
14628 		Gets the new value that just changed.
14629 	+/
14630 	@property T value() {
14631 		return getNewValue();
14632 	}
14633 
14634 	/// compatibility method for old generic Events
14635 	static if(is(immutable T == immutable int))
14636 		override int intValue() { return value; }
14637 	/// ditto
14638 	static if(is(immutable T == immutable string))
14639 		override string stringValue() { return value; }
14640 }
14641 
14642 /++
14643 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
14644 
14645 
14646 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14647 
14648 	History:
14649 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14650 +/
14651 abstract class KeyEventBase : Event {
14652 	this(string name, Widget target) {
14653 		super(name, target);
14654 	}
14655 
14656 	// for key events
14657 	Key key; ///
14658 
14659 	KeyEvent originalKeyEvent;
14660 
14661 	/++
14662 		Indicates the current state of the given keyboard modifier keys.
14663 
14664 		History:
14665 			Added to events on April 15, 2020.
14666 	+/
14667 	bool ctrlKey;
14668 
14669 	/// ditto
14670 	bool altKey;
14671 
14672 	/// ditto
14673 	bool shiftKey;
14674 
14675 	/++
14676 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
14677 
14678 		See [arsd.simpledisplay.ModifierState] for other possible flags.
14679 	+/
14680 	int state;
14681 
14682 	mixin Register;
14683 }
14684 
14685 /++
14686 	Indicates that the user has pressed a key on the keyboard, or if they've been holding it long enough to repeat (key down events are sent both on the initial press then repeated by the OS on its own time.) For available properties, see [KeyEventBase].
14687 
14688 
14689 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14690 
14691 	Please note that a `KeyDownEvent` will also often send a [CharEvent], but there is not necessarily a one-to-one relationship between them. For example, a capital letter may send KeyDownEvent for Key.Shift, then KeyDownEvent for the letter's key (this key may not match the letter due to keyboard mappings), then CharEvent for the letter, then KeyUpEvent for the letter, and finally, KeyUpEvent for shift.
14692 
14693 	For some characters, there are other key down events as well. A compose key can be pressed and released, followed by several letters pressed and released to generate one character. This is why [CharEvent] is a separate entity.
14694 
14695 	See_Also: [KeyUpEvent], [CharEvent]
14696 
14697 	History:
14698 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
14699 +/
14700 class KeyDownEvent : KeyEventBase {
14701 	enum EventString = "keydown";
14702 	this(Widget target) { super(EventString, target); }
14703 }
14704 
14705 /++
14706 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
14707 
14708 
14709 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14710 
14711 	See_Also: [KeyDownEvent], [CharEvent]
14712 
14713 	History:
14714 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
14715 +/
14716 class KeyUpEvent : KeyEventBase {
14717 	enum EventString = "keyup";
14718 	this(Widget target) { super(EventString, target); }
14719 }
14720 
14721 /++
14722 	Contains shared properties for various mouse events;
14723 
14724 
14725 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14726 
14727 	History:
14728 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14729 +/
14730 abstract class MouseEventBase : Event {
14731 	this(string name, Widget target) {
14732 		super(name, target);
14733 	}
14734 
14735 	// for mouse events
14736 	int clientX; /// The mouse event location relative to the target widget
14737 	int clientY; /// ditto
14738 
14739 	int viewportX; /// The mouse event location relative to the window origin
14740 	int viewportY; /// ditto
14741 
14742 	int button; /// See: [MouseEvent.button]
14743 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
14744 
14745 	/++
14746 		Indicates the current state of the given keyboard modifier keys.
14747 
14748 		History:
14749 			Added to mouse events on September 28, 2010.
14750 	+/
14751 	bool ctrlKey;
14752 
14753 	/// ditto
14754 	bool altKey;
14755 
14756 	/// ditto
14757 	bool shiftKey;
14758 
14759 
14760 
14761 	int state; ///
14762 
14763 	/++
14764 		for consistent names with key event.
14765 
14766 		History:
14767 			Added September 28, 2021 (dub v10.3)
14768 	+/
14769 	alias modifierState = state;
14770 
14771 	/++
14772 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
14773 
14774 		History:
14775 			Added May 15, 2021
14776 	+/
14777 	bool isMouseWheel() {
14778 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
14779 	}
14780 
14781 	// private
14782 	override void adjustClientCoordinates(int deltaX, int deltaY) {
14783 		clientX += deltaX;
14784 		clientY += deltaY;
14785 	}
14786 
14787 	override void adjustScrolling() {
14788 	version(custom_widgets) { // TEMP
14789 		viewportX = clientX;
14790 		viewportY = clientY;
14791 		if(auto se = cast(ScrollableWidget) srcElement) {
14792 			clientX += se.scrollOrigin.x;
14793 			clientY += se.scrollOrigin.y;
14794 		} else if(auto se = cast(ScrollableContainerWidget) srcElement) {
14795 			//clientX += se.scrollX_;
14796 			//clientY += se.scrollY_;
14797 		}
14798 	}
14799 	}
14800 
14801 	mixin Register;
14802 }
14803 
14804 /++
14805 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
14806 
14807 
14808 	$(WARNING
14809 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
14810 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
14811 		behavior.
14812 	)
14813 
14814 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
14815 
14816 	[MouseUpEvent] is sent when the user releases a mouse button.
14817 
14818 	[MouseMoveEvent] is sent when the mouse is moved. Please note you may not receive this in some cases unless a button is also pressed; the system is free to withhold them as an optimization. (In practice, [arsd.simpledisplay] does not request mouse motion event without a held button if it is on a remote X11 link, but does elsewhere at this time.)
14819 
14820 	[ClickEvent] is sent when the user clicks on the widget. It may also be sent with keyboard control, though minigui prefers to send a "triggered" event in addition to a mouse click and instead of a simulated mouse click in cases like keyboard activation of a button.
14821 
14822 	[DoubleClickEvent] is sent when the user clicks twice on a thing quickly, immediately after the second MouseDownEvent. The sequence is: MouseDownEvent, MouseUpEvent, ClickEvent, MouseDownEvent, DoubleClickEvent, MouseUpEvent. The second ClickEvent is NOT sent. Note that this is differnet than Javascript! They would send down,up,click,down,up,click,dblclick. Minigui does it differently because this is the way the Windows OS reports it.
14823 
14824 	[MouseOverEvent] is sent then the mouse first goes over a widget. Please note that this participates in event propagation of children! Use [MouseEnterEvent] instead if you are only interested in a specific element's whole bounding box instead of the top-most element in any particular location.
14825 
14826 	[MouseOutEvent] is sent when the mouse exits a target. Please note that this participates in event propagation of children! Use [MouseLeaveEvent] instead if you are only interested in a specific element's whole bounding box instead of the top-most element in any particular location.
14827 
14828 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
14829 
14830 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
14831 
14832 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14833 
14834 	Rationale:
14835 
14836 		If you only want to do drag, mousedown/up works just fine being consistently sent.
14837 
14838 		If you want click, that event does what you expect (if the user mouse downs then moves the mouse off the widget before going up, no click event happens - a click is only down and back up on the same thing).
14839 
14840 		If you want double click and listen to that specifically, it also just works, and if you only cared about clicks, odds are the double click should do the same thing as a single click anyway - the double was prolly accidental - so only sending the event once is prolly what user intended.
14841 
14842 	History:
14843 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on event listeners. See the member [EventString] to see what the associated string is with these elements.
14844 +/
14845 class MouseUpEvent : MouseEventBase {
14846 	enum EventString = "mouseup"; ///
14847 	this(Widget target) { super(EventString, target); }
14848 }
14849 /// ditto
14850 class MouseDownEvent : MouseEventBase {
14851 	enum EventString = "mousedown"; ///
14852 	this(Widget target) { super(EventString, target); }
14853 }
14854 /// ditto
14855 class MouseMoveEvent : MouseEventBase {
14856 	enum EventString = "mousemove"; ///
14857 	this(Widget target) { super(EventString, target); }
14858 }
14859 /// ditto
14860 class ClickEvent : MouseEventBase {
14861 	enum EventString = "click"; ///
14862 	this(Widget target) { super(EventString, target); }
14863 }
14864 /// ditto
14865 class DoubleClickEvent : MouseEventBase {
14866 	enum EventString = "dblclick"; ///
14867 	this(Widget target) { super(EventString, target); }
14868 }
14869 /// ditto
14870 class MouseOverEvent : Event {
14871 	enum EventString = "mouseover"; ///
14872 	this(Widget target) { super(EventString, target); }
14873 }
14874 /// ditto
14875 class MouseOutEvent : Event {
14876 	enum EventString = "mouseout"; ///
14877 	this(Widget target) { super(EventString, target); }
14878 }
14879 /// ditto
14880 class MouseEnterEvent : Event {
14881 	enum EventString = "mouseenter"; ///
14882 	this(Widget target) { super(EventString, target); }
14883 
14884 	override bool propagates() const { return false; }
14885 }
14886 /// ditto
14887 class MouseLeaveEvent : Event {
14888 	enum EventString = "mouseleave"; ///
14889 	this(Widget target) { super(EventString, target); }
14890 
14891 	override bool propagates() const { return false; }
14892 }
14893 
14894 private bool isAParentOf(Widget a, Widget b) {
14895 	if(a is null || b is null)
14896 		return false;
14897 
14898 	while(b !is null) {
14899 		if(a is b)
14900 			return true;
14901 		b = b.parent;
14902 	}
14903 
14904 	return false;
14905 }
14906 
14907 private struct WidgetAtPointResponse {
14908 	Widget widget;
14909 
14910 	// x, y relative to the widget in the response.
14911 	int x;
14912 	int y;
14913 }
14914 
14915 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
14916 	assert(starting !is null);
14917 
14918 	starting.addScrollPosition(x, y);
14919 
14920 	auto child = starting.getChildAtPosition(x, y);
14921 	while(child) {
14922 		if(child.hidden)
14923 			continue;
14924 		starting = child;
14925 		x -= child.x;
14926 		y -= child.y;
14927 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
14928 		child = r.widget;
14929 		if(child is starting)
14930 			break;
14931 	}
14932 	return WidgetAtPointResponse(starting, x, y);
14933 }
14934 
14935 version(win32_widgets) {
14936 private:
14937 	import core.sys.windows.commctrl;
14938 
14939 	pragma(lib, "comctl32");
14940 	shared static this() {
14941 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
14942 		INITCOMMONCONTROLSEX ic;
14943 		ic.dwSize = cast(DWORD) ic.sizeof;
14944 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
14945 		if(!InitCommonControlsEx(&ic)) {
14946 			//writeln("ICC failed");
14947 		}
14948 	}
14949 
14950 
14951 	// everything from here is just win32 headers copy pasta
14952 private:
14953 extern(Windows):
14954 
14955 	alias HANDLE HMENU;
14956 	HMENU CreateMenu();
14957 	bool SetMenu(HWND, HMENU);
14958 	HMENU CreatePopupMenu();
14959 	enum MF_POPUP = 0x10;
14960 	enum MF_STRING = 0;
14961 
14962 
14963 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
14964 	struct INITCOMMONCONTROLSEX {
14965 		DWORD dwSize;
14966 		DWORD dwICC;
14967 	}
14968 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
14969 enum {
14970         IDB_STD_SMALL_COLOR,
14971         IDB_STD_LARGE_COLOR,
14972         IDB_VIEW_SMALL_COLOR = 4,
14973         IDB_VIEW_LARGE_COLOR = 5
14974 }
14975 enum {
14976         STD_CUT,
14977         STD_COPY,
14978         STD_PASTE,
14979         STD_UNDO,
14980         STD_REDOW,
14981         STD_DELETE,
14982         STD_FILENEW,
14983         STD_FILEOPEN,
14984         STD_FILESAVE,
14985         STD_PRINTPRE,
14986         STD_PROPERTIES,
14987         STD_HELP,
14988         STD_FIND,
14989         STD_REPLACE,
14990         STD_PRINT // = 14
14991 }
14992 
14993 alias HANDLE HIMAGELIST;
14994 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
14995 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
14996         BOOL ImageList_Destroy(HIMAGELIST);
14997 
14998 uint MAKELONG(ushort a, ushort b) {
14999         return cast(uint) ((b << 16) | a);
15000 }
15001 
15002 
15003 struct TBBUTTON {
15004 	int   iBitmap;
15005 	int   idCommand;
15006 	BYTE  fsState;
15007 	BYTE  fsStyle;
15008 	version(Win64)
15009 	BYTE[6] bReserved;
15010 	else
15011 	BYTE[2]  bReserved;
15012 	DWORD dwData;
15013 	INT_PTR   iString;
15014 }
15015 
15016 	enum {
15017 		TB_ADDBUTTONSA   = WM_USER + 20,
15018 		TB_INSERTBUTTONA = WM_USER + 21,
15019 		TB_GETIDEALSIZE = WM_USER + 99,
15020 	}
15021 
15022 struct SIZE {
15023 	LONG cx;
15024 	LONG cy;
15025 }
15026 
15027 
15028 enum {
15029 	TBSTATE_CHECKED       = 1,
15030 	TBSTATE_PRESSED       = 2,
15031 	TBSTATE_ENABLED       = 4,
15032 	TBSTATE_HIDDEN        = 8,
15033 	TBSTATE_INDETERMINATE = 16,
15034 	TBSTATE_WRAP          = 32
15035 }
15036 
15037 
15038 
15039 enum {
15040 	ILC_COLOR    = 0,
15041 	ILC_COLOR4   = 4,
15042 	ILC_COLOR8   = 8,
15043 	ILC_COLOR16  = 16,
15044 	ILC_COLOR24  = 24,
15045 	ILC_COLOR32  = 32,
15046 	ILC_COLORDDB = 254,
15047 	ILC_MASK     = 1,
15048 	ILC_PALETTE  = 2048
15049 }
15050 
15051 
15052 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
15053 
15054 
15055 enum {
15056 	TB_ENABLEBUTTON          = WM_USER + 1,
15057 	TB_CHECKBUTTON,
15058 	TB_PRESSBUTTON,
15059 	TB_HIDEBUTTON,
15060 	TB_INDETERMINATE, //     = WM_USER + 5,
15061 	TB_ISBUTTONENABLED       = WM_USER + 9,
15062 	TB_ISBUTTONCHECKED,
15063 	TB_ISBUTTONPRESSED,
15064 	TB_ISBUTTONHIDDEN,
15065 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
15066 	TB_SETSTATE              = WM_USER + 17,
15067 	TB_GETSTATE              = WM_USER + 18,
15068 	TB_ADDBITMAP             = WM_USER + 19,
15069 	TB_DELETEBUTTON          = WM_USER + 22,
15070 	TB_GETBUTTON,
15071 	TB_BUTTONCOUNT,
15072 	TB_COMMANDTOINDEX,
15073 	TB_SAVERESTOREA,
15074 	TB_CUSTOMIZE,
15075 	TB_ADDSTRINGA,
15076 	TB_GETITEMRECT,
15077 	TB_BUTTONSTRUCTSIZE,
15078 	TB_SETBUTTONSIZE,
15079 	TB_SETBITMAPSIZE,
15080 	TB_AUTOSIZE, //          = WM_USER + 33,
15081 	TB_GETTOOLTIPS           = WM_USER + 35,
15082 	TB_SETTOOLTIPS           = WM_USER + 36,
15083 	TB_SETPARENT             = WM_USER + 37,
15084 	TB_SETROWS               = WM_USER + 39,
15085 	TB_GETROWS,
15086 	TB_GETBITMAPFLAGS,
15087 	TB_SETCMDID,
15088 	TB_CHANGEBITMAP,
15089 	TB_GETBITMAP,
15090 	TB_GETBUTTONTEXTA,
15091 	TB_REPLACEBITMAP, //     = WM_USER + 46,
15092 	TB_GETBUTTONSIZE         = WM_USER + 58,
15093 	TB_SETBUTTONWIDTH        = WM_USER + 59,
15094 	TB_GETBUTTONTEXTW        = WM_USER + 75,
15095 	TB_SAVERESTOREW          = WM_USER + 76,
15096 	TB_ADDSTRINGW            = WM_USER + 77,
15097 }
15098 
15099 extern(Windows)
15100 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
15101 
15102 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
15103 
15104 
15105 	enum {
15106 		TB_SETINDENT = WM_USER + 47,
15107 		TB_SETIMAGELIST,
15108 		TB_GETIMAGELIST,
15109 		TB_LOADIMAGES,
15110 		TB_GETRECT,
15111 		TB_SETHOTIMAGELIST,
15112 		TB_GETHOTIMAGELIST,
15113 		TB_SETDISABLEDIMAGELIST,
15114 		TB_GETDISABLEDIMAGELIST,
15115 		TB_SETSTYLE,
15116 		TB_GETSTYLE,
15117 		//TB_GETBUTTONSIZE,
15118 		//TB_SETBUTTONWIDTH,
15119 		TB_SETMAXTEXTROWS,
15120 		TB_GETTEXTROWS // = WM_USER + 61
15121 	}
15122 
15123 enum {
15124 	CCM_FIRST            = 0x2000,
15125 	CCM_LAST             = CCM_FIRST + 0x200,
15126 	CCM_SETBKCOLOR       = 8193,
15127 	CCM_SETCOLORSCHEME   = 8194,
15128 	CCM_GETCOLORSCHEME   = 8195,
15129 	CCM_GETDROPTARGET    = 8196,
15130 	CCM_SETUNICODEFORMAT = 8197,
15131 	CCM_GETUNICODEFORMAT = 8198,
15132 	CCM_SETVERSION       = 0x2007,
15133 	CCM_GETVERSION       = 0x2008,
15134 	CCM_SETNOTIFYWINDOW  = 0x2009
15135 }
15136 
15137 
15138 enum {
15139 	PBM_SETRANGE     = WM_USER + 1,
15140 	PBM_SETPOS,
15141 	PBM_DELTAPOS,
15142 	PBM_SETSTEP,
15143 	PBM_STEPIT,   // = WM_USER + 5
15144 	PBM_SETRANGE32   = 1030,
15145 	PBM_GETRANGE,
15146 	PBM_GETPOS,
15147 	PBM_SETBARCOLOR, // = 1033
15148 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
15149 }
15150 
15151 enum {
15152 	PBS_SMOOTH   = 1,
15153 	PBS_VERTICAL = 4
15154 }
15155 
15156 enum {
15157         ICC_LISTVIEW_CLASSES = 1,
15158         ICC_TREEVIEW_CLASSES = 2,
15159         ICC_BAR_CLASSES      = 4,
15160         ICC_TAB_CLASSES      = 8,
15161         ICC_UPDOWN_CLASS     = 16,
15162         ICC_PROGRESS_CLASS   = 32,
15163         ICC_HOTKEY_CLASS     = 64,
15164         ICC_ANIMATE_CLASS    = 128,
15165         ICC_WIN95_CLASSES    = 255,
15166         ICC_DATE_CLASSES     = 256,
15167         ICC_USEREX_CLASSES   = 512,
15168         ICC_COOL_CLASSES     = 1024,
15169 	ICC_STANDARD_CLASSES = 0x00004000,
15170 }
15171 
15172 	enum WM_USER = 1024;
15173 }
15174 
15175 version(win32_widgets)
15176 	pragma(lib, "comdlg32");
15177 
15178 
15179 ///
15180 enum GenericIcons : ushort {
15181 	None, ///
15182 	// these happen to match the win32 std icons numerically if you just subtract one from the value
15183 	Cut, ///
15184 	Copy, ///
15185 	Paste, ///
15186 	Undo, ///
15187 	Redo, ///
15188 	Delete, ///
15189 	New, ///
15190 	Open, ///
15191 	Save, ///
15192 	PrintPreview, ///
15193 	Properties, ///
15194 	Help, ///
15195 	Find, ///
15196 	Replace, ///
15197 	Print, ///
15198 }
15199 
15200 enum FileDialogType {
15201 	Automatic,
15202 	Open,
15203 	Save
15204 }
15205 string previousFileReferenced;
15206 
15207 /++
15208 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
15209 
15210 	Params:
15211 		storage = an alias to a `static string` variable that stores the last file referenced. It will
15212 		use this to pre-fill the dialog with a suggestion.
15213 
15214 		Please note that it MUST be `static` or you will get compile errors.
15215 
15216 		filters = the filters param to [getFileName]
15217 
15218 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
15219 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
15220 		a save dialog box. Otherwise, it will show an open dialog box.
15221 +/
15222 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
15223 	string name;
15224 	alias name this;
15225 }
15226 
15227 /++
15228 	Gets a file name for an open or save operation, calling your `onOK` function when the user has selected one. This function may or may not block depending on the operating system, you MUST assume it will complete asynchronously.
15229 
15230 	History:
15231 		onCancel was added November 6, 2021.
15232 
15233 		The dialog itself on Linux was modified on December 2, 2021 to include
15234 		a directory picker in addition to the command line completion view.
15235 
15236 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
15237 	Future_directions:
15238 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
15239 		at least on Linux, maybe on Windows too.
15240 +/
15241 void getOpenFileName(
15242 	void delegate(string) onOK,
15243 	string prefilledName = null,
15244 	string[] filters = null,
15245 	void delegate() onCancel = null,
15246 	string initialDirectory = null,
15247 )
15248 {
15249 	return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory);
15250 }
15251 
15252 /// ditto
15253 void getSaveFileName(
15254 	void delegate(string) onOK,
15255 	string prefilledName = null,
15256 	string[] filters = null,
15257 	void delegate() onCancel = null,
15258 	string initialDirectory = null,
15259 )
15260 {
15261 	return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory);
15262 }
15263 
15264 void getFileName(
15265 	bool openOrSave,
15266 	void delegate(string) onOK,
15267 	string prefilledName = null,
15268 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
15269 	void delegate() onCancel = null,
15270 	string initialDirectory = null,
15271 )
15272 {
15273 
15274 	version(win32_widgets) {
15275 		import core.sys.windows.commdlg;
15276 	/*
15277 	Ofn.lStructSize = sizeof(OPENFILENAME);
15278 	Ofn.hwndOwner = hWnd;
15279 	Ofn.lpstrFilter = szFilter;
15280 	Ofn.lpstrFile= szFile;
15281 	Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
15282 	Ofn.lpstrFileTitle = szFileTitle;
15283 	Ofn.nMaxFileTitle = sizeof(szFileTitle);
15284 	Ofn.lpstrInitialDir = (LPSTR)NULL;
15285 	Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
15286 	Ofn.lpstrTitle = szTitle;
15287 	 */
15288 
15289 
15290 		wchar[1024] file = 0;
15291 		wchar[1024] filterBuffer = 0;
15292 		makeWindowsString(prefilledName, file[]);
15293 		OPENFILENAME ofn;
15294 		ofn.lStructSize = ofn.sizeof;
15295 		if(filters.length) {
15296 			string filter;
15297 			foreach(i, f; filters) {
15298 				filter ~= f;
15299 				filter ~= "\0";
15300 			}
15301 			filter ~= "\0";
15302 			ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
15303 		}
15304 		ofn.lpstrFile = file.ptr;
15305 		ofn.nMaxFile = file.length;
15306 
15307 		wchar[1024] initialDir = 0;
15308 		if(initialDirectory !is null) {
15309 			makeWindowsString(initialDirectory, initialDir[]);
15310 			ofn.lpstrInitialDir = file.ptr;
15311 		}
15312 
15313 		if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
15314 		{
15315 			string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
15316 			if(okString.length && okString[$-1] == '\0')
15317 				okString = okString[0..$-1];
15318 			onOK(okString);
15319 		} else {
15320 			if(onCancel)
15321 				onCancel();
15322 		}
15323 	} else version(custom_widgets) {
15324 		if(filters.length == 0)
15325 			filters = ["All Files\0*.*"];
15326 		auto picker = new FilePicker(prefilledName, filters, initialDirectory);
15327 		picker.onOK = onOK;
15328 		picker.onCancel = onCancel;
15329 		picker.show();
15330 	}
15331 }
15332 
15333 version(custom_widgets)
15334 private
15335 class FilePicker : Dialog {
15336 	void delegate(string) onOK;
15337 	void delegate() onCancel;
15338 	LineEdit lineEdit;
15339 
15340 	// returns common prefix
15341 	string loadFiles(string cwd, string[] filters...) {
15342 		string[] files;
15343 		string[] dirs;
15344 
15345 		string commonPrefix;
15346 
15347 		getFiles(cwd, (string name, bool isDirectory) {
15348 			if(name == ".")
15349 				return; // skip this as unnecessary
15350 			if(isDirectory)
15351 				dirs ~= name;
15352 			else {
15353 				foreach(filter; filters)
15354 				if(
15355 					filter.length <= 1 ||
15356 					filter == "*.*" ||
15357 					(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
15358 					(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
15359 				)
15360 				{
15361 					files ~= name;
15362 
15363 					if(filter.length > 0 && filter[$-1] == '*') {
15364 						if(commonPrefix is null) {
15365 							commonPrefix = name;
15366 						} else {
15367 							foreach(idx, char i; name) {
15368 								if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
15369 									commonPrefix = commonPrefix[0 .. idx];
15370 									break;
15371 								}
15372 							}
15373 						}
15374 					}
15375 
15376 					break;
15377 				}
15378 			}
15379 		});
15380 
15381 		extern(C) static int comparator(scope const void* a, scope const void* b) {
15382 			auto sa = *cast(string*) a;
15383 			auto sb = *cast(string*) b;
15384 
15385 			for(int i = 0; i < sa.length; i++) {
15386 				if(i == sb.length)
15387 					return 1;
15388 				auto diff = sa[i] - sb[i];
15389 				if(diff)
15390 					return diff;
15391 			}
15392 
15393 			return 0;
15394 		}
15395 
15396 		nonPhobosSort(files, &comparator);
15397 		nonPhobosSort(dirs, &comparator);
15398 
15399 		listWidget.clear();
15400 		dirWidget.clear();
15401 		foreach(name; dirs)
15402 			dirWidget.addOption(name);
15403 		foreach(name; files)
15404 			listWidget.addOption(name);
15405 
15406 		return commonPrefix;
15407 	}
15408 
15409 	ListWidget listWidget;
15410 	ListWidget dirWidget;
15411 
15412 	string currentDirectory;
15413 	string[] processedFilters;
15414 
15415 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"]
15416 	this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) {
15417 		super(300, 200, "Choose File..."); // owner);
15418 
15419 		foreach(filter; filters) {
15420 			while(filter.length && filter[0] != 0) {
15421 				filter = filter[1 .. $];
15422 			}
15423 			if(filter.length)
15424 				filter = filter[1 .. $]; // trim off the 0
15425 
15426 			while(filter.length) {
15427 				int idx = 0;
15428 				while(idx < filter.length && filter[idx] != ';') {
15429 					idx++;
15430 				}
15431 
15432 				processedFilters ~= filter[0 .. idx];
15433 				if(idx < filter.length)
15434 					idx++; // skip the ;
15435 				filter = filter[idx .. $];
15436 			}
15437 		}
15438 
15439 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
15440 
15441 		{
15442 			auto hl = new HorizontalLayout(this);
15443 			dirWidget = new ListWidget(hl);
15444 			listWidget = new ListWidget(hl);
15445 
15446 			// double click events normally trigger something else but
15447 			// here user might be clicking kinda fast and we'd rather just
15448 			// keep it
15449 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
15450 				auto ce = new ChangeEvent!void(dirWidget, () {});
15451 				ce.dispatch();
15452 			});
15453 
15454 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
15455 				string v;
15456 				foreach(o; dirWidget.options)
15457 					if(o.selected) {
15458 						v = o.label;
15459 						break;
15460 					}
15461 				if(v.length) {
15462 					currentDirectory ~= "/" ~ v;
15463 					loadFiles(currentDirectory, processedFilters);
15464 				}
15465 			});
15466 
15467 			// double click here, on the other hand, selects the file
15468 			// and moves on
15469 			listWidget.addEventListener((scope DoubleClickEvent dev) {
15470 				OK();
15471 			});
15472 		}
15473 
15474 		lineEdit = new LineEdit(this);
15475 		lineEdit.focus();
15476 		lineEdit.addEventListener(delegate(CharEvent event) {
15477 			if(event.character == '\t' || event.character == '\n')
15478 				event.preventDefault();
15479 		});
15480 
15481 		listWidget.addEventListener(EventType.change, () {
15482 			foreach(o; listWidget.options)
15483 				if(o.selected)
15484 					lineEdit.content = o.label;
15485 		});
15486 
15487 		loadFiles(currentDirectory, processedFilters);
15488 
15489 		lineEdit.addEventListener((KeyDownEvent event) {
15490 			if(event.key == Key.Tab) {
15491 
15492 				auto current = lineEdit.content;
15493 				if(current.length >= 2 && current[0 ..2] == "./")
15494 					current = current[2 .. $];
15495 
15496 				auto commonPrefix = loadFiles(currentDirectory, current ~ "*");
15497 
15498 				if(commonPrefix.length)
15499 					lineEdit.content = commonPrefix;
15500 
15501 				// FIXME: if that is a directory, add the slash? or even go inside?
15502 
15503 				event.preventDefault();
15504 			}
15505 		});
15506 
15507 		lineEdit.content = prefilledName;
15508 
15509 		auto hl = new HorizontalLayout(60, this);
15510 		auto cancelButton = new Button("Cancel", hl);
15511 		auto okButton = new Button("OK", hl);
15512 
15513 		cancelButton.addEventListener(EventType.triggered, &Cancel);
15514 		okButton.addEventListener(EventType.triggered, &OK);
15515 
15516 		this.addEventListener((KeyDownEvent event) {
15517 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
15518 				event.preventDefault();
15519 				OK();
15520 			}
15521 			if(event.key == Key.Escape)
15522 				Cancel();
15523 		});
15524 
15525 	}
15526 
15527 	override void OK() {
15528 		if(lineEdit.content.length) {
15529 			string accepted;
15530 			auto c = lineEdit.content;
15531 			if(c.length && c[0] == '/')
15532 				accepted = c;
15533 			else
15534 				accepted = currentDirectory ~ "/" ~ lineEdit.content;
15535 
15536 			if(isDir(accepted)) {
15537 				// FIXME: would be kinda nice to support ~ and collapse these paths too
15538 				// FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later.
15539 				currentDirectory = accepted;
15540 				loadFiles(currentDirectory, processedFilters);
15541 				lineEdit.content = "";
15542 				return;
15543 			}
15544 
15545 			if(onOK)
15546 				onOK(accepted);
15547 		}
15548 		close();
15549 	}
15550 
15551 	override void Cancel() {
15552 		if(onCancel)
15553 			onCancel();
15554 		close();
15555 	}
15556 }
15557 
15558 private bool isDir(string name) {
15559 	version(Windows) {
15560 		auto ws = WCharzBuffer(name);
15561 		auto ret = GetFileAttributesW(ws.ptr);
15562 		if(ret == INVALID_FILE_ATTRIBUTES)
15563 			return false;
15564 		return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0;
15565 	} else version(Posix) {
15566 		import core.sys.posix.sys.stat;
15567 		stat_t buf;
15568 		auto ret = stat((name ~ '\0').ptr, &buf);
15569 		if(ret == -1)
15570 			return false; // I could probably check more specific errors tbh
15571 		return (buf.st_mode & S_IFMT) == S_IFDIR;
15572 	} else return false;
15573 }
15574 
15575 /*
15576 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
15577 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
15578 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
15579 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
15580 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
15581 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
15582 http://www.sbin.org/doc/Xlib/chapt_03.html
15583 
15584 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
15585 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
15586 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
15587 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
15588 */
15589 
15590 
15591 // These are all for setMenuAndToolbarFromAnnotatedCode
15592 /// This item in the menu will be preceded by a separator line
15593 /// Group: generating_from_code
15594 struct separator {}
15595 deprecated("It was misspelled, use separator instead") alias seperator = separator;
15596 /// Program-wide keyboard shortcut to trigger the action
15597 /// Group: generating_from_code
15598 struct accelerator { string keyString; }
15599 /// tells which menu the action will be on
15600 /// Group: generating_from_code
15601 struct menu { string name; }
15602 /// Describes which toolbar section the action appears on
15603 /// Group: generating_from_code
15604 struct toolbar { string groupName; }
15605 ///
15606 /// Group: generating_from_code
15607 struct icon { ushort id; }
15608 ///
15609 /// Group: generating_from_code
15610 struct label { string label; }
15611 ///
15612 /// Group: generating_from_code
15613 struct hotkey { dchar ch; }
15614 ///
15615 /// Group: generating_from_code
15616 struct tip { string tip; }
15617 
15618 
15619 /++
15620 	Observes and allows inspection of an object via automatic gui
15621 +/
15622 /// Group: generating_from_code
15623 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
15624 	return new ObjectInspectionWindowImpl!(T)(t);
15625 }
15626 
15627 class ObjectInspectionWindow : Window {
15628 	this(int a, int b, string c) {
15629 		super(a, b, c);
15630 	}
15631 
15632 	abstract void readUpdatesFromObject();
15633 }
15634 
15635 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
15636 	T t;
15637 	this(T t) {
15638 		this.t = t;
15639 
15640 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
15641 
15642 		foreach(memberName; __traits(derivedMembers, T)) {{
15643 			alias member = I!(__traits(getMember, t, memberName))[0];
15644 			alias type = typeof(member);
15645 			static if(is(type == int)) {
15646 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
15647 				//le.addEventListener("char", (Event ev) {
15648 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
15649 						//ev.preventDefault();
15650 				//});
15651 				le.addEventListener(EventType.change, (Event ev) {
15652 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
15653 				});
15654 
15655 				updateMemberDelegates[memberName] = () {
15656 					le.content = toInternal!string(__traits(getMember, t, memberName));
15657 				};
15658 			}
15659 		}}
15660 	}
15661 
15662 	void delegate()[string] updateMemberDelegates;
15663 
15664 	override void readUpdatesFromObject() {
15665 		foreach(k, v; updateMemberDelegates)
15666 			v();
15667 	}
15668 }
15669 
15670 /++
15671 	Creates a dialog based on a data structure.
15672 
15673 	---
15674 	dialog((YourStructure value) {
15675 		// the user filled in the struct and clicked OK,
15676 		// you can check the members now
15677 	});
15678 	---
15679 
15680 	Params:
15681 		initialData = the initial value to show in the dialog. It will not modify this unless
15682 		it is a class then it might, no promises.
15683 
15684 	History:
15685 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
15686 +/
15687 /// Group: generating_from_code
15688 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15689 	dialog(T.init, onOK, onCancel, title);
15690 }
15691 /// ditto
15692 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15693 	auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title);
15694 	dg.show();
15695 }
15696 
15697 private static template I(T...) { alias I = T; }
15698 
15699 
15700 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
15701 	if(name == "id")
15702 		return allLowerCase ? name : "ID";
15703 
15704 	char[160] buffer;
15705 	int bufferIndex = 0;
15706 	bool shouldCap = true;
15707 	bool shouldSpace;
15708 	bool lastWasCap;
15709 	foreach(idx, char ch; name) {
15710 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15711 
15712 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
15713 			if(lastWasCap) {
15714 				// two caps in a row, don't change. Prolly acronym.
15715 			} else {
15716 				if(idx)
15717 					shouldSpace = true; // new word, add space
15718 			}
15719 
15720 			lastWasCap = true;
15721 		} else {
15722 			lastWasCap = false;
15723 		}
15724 
15725 		if(shouldSpace) {
15726 			buffer[bufferIndex++] = space;
15727 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15728 			shouldSpace = false;
15729 		}
15730 		if(shouldCap) {
15731 			if(ch >= 'a' && ch <= 'z')
15732 				ch -= 32;
15733 			shouldCap = false;
15734 		}
15735 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
15736 			ch += 32;
15737 		buffer[bufferIndex++] = ch;
15738 	}
15739 	return buffer[0 .. bufferIndex].idup;
15740 }
15741 
15742 /++
15743 	This is the implementation for [dialog]. None of its details are guaranteed stable and may change at any time; the stable interface is just the [dialog] function at this time.
15744 +/
15745 class AutomaticDialog(T) : Dialog {
15746 	T t;
15747 
15748 	void delegate(T) onOK;
15749 	void delegate() onCancel;
15750 
15751 	override int paddingTop() { return defaultLineHeight; }
15752 	override int paddingBottom() { return defaultLineHeight; }
15753 	override int paddingRight() { return defaultLineHeight; }
15754 	override int paddingLeft() { return defaultLineHeight; }
15755 
15756 	this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
15757 		assert(onOK !is null);
15758 
15759 		t = initialData;
15760 
15761 		static if(is(T == class)) {
15762 			if(t is null)
15763 				t = new T();
15764 		}
15765 		this.onOK = onOK;
15766 		this.onCancel = onCancel;
15767 		super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
15768 
15769 		static if(is(T == class))
15770 			this.addDataControllerWidget(t);
15771 		else
15772 			this.addDataControllerWidget(&t);
15773 
15774 		auto hl = new HorizontalLayout(this);
15775 		auto stretch = new HorizontalSpacer(hl); // to right align
15776 		auto ok = new CommandButton("OK", hl);
15777 		auto cancel = new CommandButton("Cancel", hl);
15778 		ok.addEventListener(EventType.triggered, &OK);
15779 		cancel.addEventListener(EventType.triggered, &Cancel);
15780 
15781 		this.addEventListener((KeyDownEvent ev) {
15782 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
15783 				ok.focus();
15784 				OK();
15785 				ev.preventDefault();
15786 			}
15787 			if(ev.key == Key.Escape) {
15788 				Cancel();
15789 				ev.preventDefault();
15790 			}
15791 		});
15792 
15793 		this.addEventListener((scope ClosedEvent ce) {
15794 			if(onCancel)
15795 				onCancel();
15796 		});
15797 
15798 		//this.children[0].focus();
15799 	}
15800 
15801 	override void OK() {
15802 		onOK(t);
15803 		close();
15804 	}
15805 
15806 	override void Cancel() {
15807 		if(onCancel)
15808 			onCancel();
15809 		close();
15810 	}
15811 }
15812 
15813 private template baseClassCount(Class) {
15814 	private int helper() {
15815 		int count = 0;
15816 		static if(is(Class bases == super)) {
15817 			foreach(base; bases)
15818 				static if(is(base == class))
15819 					count += 1 + baseClassCount!base;
15820 		}
15821 		return count;
15822 	}
15823 
15824 	enum int baseClassCount = helper();
15825 }
15826 
15827 private long stringToLong(string s) {
15828 	long ret;
15829 	if(s.length == 0)
15830 		return ret;
15831 	bool negative = s[0] == '-';
15832 	if(negative)
15833 		s = s[1 .. $];
15834 	foreach(ch; s) {
15835 		if(ch >= '0' && ch <= '9') {
15836 			ret *= 10;
15837 			ret += ch - '0';
15838 		}
15839 	}
15840 	if(negative)
15841 		ret = -ret;
15842 	return ret;
15843 }
15844 
15845 
15846 interface ReflectableProperties {
15847 	/++
15848 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
15849 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
15850 		json in the current implementation.
15851 
15852 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
15853 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
15854 		as of the June 2, 2021 release.
15855 
15856 		History:
15857 			Added June 2, 2021.
15858 
15859 		See_Also: [getPropertyAsString], [setPropertyFromString]
15860 	+/
15861 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
15862 	/++
15863 		Requests a property to be delivered to you as a string, through your `sink` delegate.
15864 
15865 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
15866 		be interpreted as json, otherwise, it is just a plain string.
15867 
15868 		The sink should always be called exactly once for each call (it is basically a return value, but it might
15869 		use a local buffer it maintains instead of allocating a return value).
15870 
15871 		History:
15872 			Added June 2, 2021.
15873 
15874 		See_Also: [getPropertiesList], [setPropertyFromString]
15875 	+/
15876 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
15877 	/++
15878 		Sets the given property, if it exists, to the given value, if possible. If `strIsJson` is true, it will json decode (if the implementation wants to) then apply the value, otherwise it will treat it as a plain string.
15879 
15880 		History:
15881 			Added June 2, 2021.
15882 
15883 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
15884 	+/
15885 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
15886 
15887 	/// [setPropertyFromString] possible return values
15888 	enum SetPropertyResult {
15889 		success = 0, /// the property has been successfully set to the request value
15890 		notPermitted = -1, /// the property exists but it cannot be changed at this time
15891 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
15892 		noSuchProperty = -3, /// there is no property by that name
15893 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
15894 		invalidValue = -5, /// the string is in the correct format, but the specific given value could not be used (for example, because it was out of bounds)
15895 	}
15896 
15897 	/++
15898 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
15899 
15900 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
15901 
15902 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
15903 		rarely need to use these building blocks directly.
15904 	+/
15905 	mixin template RegisterSetters() {
15906 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
15907 			switch(name) {
15908 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
15909 					case memberName:
15910 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
15911 							if(value != "true" && value != "false")
15912 								return SetPropertyResult.wrongFormat;
15913 							__traits(getMember, this, memberName) = value == "true" ? true : false;
15914 							return SetPropertyResult.success;
15915 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
15916 							import core.stdc.stdlib;
15917 							char[128] zero = 0;
15918 							if(buffer.length + 1 >= zero.length)
15919 								return SetPropertyResult.wrongFormat;
15920 							zero[0 .. buffer.length] = buffer[];
15921 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
15922 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
15923 							import core.stdc.stdlib;
15924 							char[128] zero = 0;
15925 							if(buffer.length + 1 >= zero.length)
15926 								return SetPropertyResult.wrongFormat;
15927 							zero[0 .. buffer.length] = buffer[];
15928 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
15929 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
15930 							__traits(getMember, this, memberName) = value.idup;
15931 						} else {
15932 							return SetPropertyResult.notImplemented;
15933 						}
15934 
15935 				}
15936 				default:
15937 					return super.setPropertyFromString(name, value, valueIsJson);
15938 			}
15939 		}
15940 	}
15941 
15942 	/++
15943 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
15944 
15945 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
15946 
15947 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
15948 		rarely need to use these building blocks directly.
15949 	+/
15950 	mixin template RegisterGetters() {
15951 		override void getPropertiesList(scope void delegate(string name) sink) const {
15952 			super.getPropertiesList(sink);
15953 
15954 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
15955 				sink(memberName);
15956 			}
15957 		}
15958 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
15959 			switch(name) {
15960 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
15961 					case memberName:
15962 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
15963 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
15964 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
15965 							import core.stdc.stdio;
15966 							char[32] buffer;
15967 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
15968 							sink(name, buffer[0 .. len], true);
15969 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
15970 							import core.stdc.stdio;
15971 							char[32] buffer;
15972 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
15973 							sink(name, buffer[0 .. len], true);
15974 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
15975 							sink(name, __traits(getMember, this, memberName), false);
15976 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
15977 						} else {
15978 							sink(name, null, true);
15979 						}
15980 
15981 					return;
15982 				}
15983 				default:
15984 					return super.getPropertyAsString(name, sink);
15985 			}
15986 		}
15987 	}
15988 }
15989 
15990 private struct Stack(T) {
15991 	this(int maxSize) {
15992 		internalLength = 0;
15993 		arr = initialBuffer[];
15994 	}
15995 
15996 	///.
15997 	void push(T t) {
15998 		if(internalLength >= arr.length) {
15999 			auto oldarr = arr;
16000 			if(arr.length < 4096)
16001 				arr = new T[arr.length * 2];
16002 			else
16003 				arr = new T[arr.length + 4096];
16004 			arr[0 .. oldarr.length] = oldarr[];
16005 		}
16006 
16007 		arr[internalLength] = t;
16008 		internalLength++;
16009 	}
16010 
16011 	///.
16012 	T pop() {
16013 		assert(internalLength);
16014 		internalLength--;
16015 		return arr[internalLength];
16016 	}
16017 
16018 	///.
16019 	T peek() {
16020 		assert(internalLength);
16021 		return arr[internalLength - 1];
16022 	}
16023 
16024 	///.
16025 	@property bool empty() {
16026 		return internalLength ? false : true;
16027 	}
16028 
16029 	///.
16030 	private T[] arr;
16031 	private size_t internalLength;
16032 	private T[64] initialBuffer;
16033 	// the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep),
16034 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
16035 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
16036 }
16037 
16038 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively.
16039 private struct WidgetStream {
16040 
16041 	///.
16042 	@property Widget front() {
16043 		return current.widget;
16044 	}
16045 
16046 	/// Use Widget.tree instead.
16047 	this(Widget start) {
16048 		current.widget = start;
16049 		current.childPosition = -1;
16050 		isEmpty = false;
16051 		stack = typeof(stack)(0);
16052 	}
16053 
16054 	/*
16055 		Handle it
16056 		handle its children
16057 
16058 	*/
16059 
16060 	///.
16061 	void popFront() {
16062 	    more:
16063 	    	if(isEmpty) return;
16064 
16065 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
16066 
16067 		current.childPosition++;
16068 		if(current.childPosition >= current.widget.children.length) {
16069 			if(stack.empty())
16070 				isEmpty = true;
16071 			else {
16072 				current = stack.pop();
16073 				goto more;
16074 			}
16075 		} else {
16076 			stack.push(current);
16077 			current.widget = current.widget.children[current.childPosition];
16078 			current.childPosition = -1;
16079 		}
16080 	}
16081 
16082 	///.
16083 	@property bool empty() {
16084 		return isEmpty;
16085 	}
16086 
16087 	private:
16088 
16089 	struct Current {
16090 		Widget widget;
16091 		int childPosition;
16092 	}
16093 
16094 	Current current;
16095 
16096 	Stack!(Current) stack;
16097 
16098 	bool isEmpty;
16099 }
16100 
16101 
16102 /+
16103 
16104 	I could fix up the hierarchy kinda like this
16105 
16106 	class Widget {
16107 		Widget[] children() { return null; }
16108 	}
16109 	interface WidgetContainer {
16110 		Widget asWidget();
16111 		void addChild(Widget w);
16112 
16113 		// alias asWidget this; // but meh
16114 	}
16115 
16116 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
16117 
16118 	class Layout : Widget, WidgetContainer {}
16119 
16120 	class Window : WidgetContainer {}
16121 
16122 
16123 	All constructors that previously took Widgets should now take WidgetContainers instead
16124 
16125 
16126 
16127 	But I'm kinda meh toward it, im not sure this is a real problem even though there are some addChild things that throw "plz don't".
16128 +/
16129 
16130 /+
16131 	LAYOUTS 2.0
16132 
16133 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
16134 
16135 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
16136 
16137 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
16138 
16139 	and even Paint can just use computedStyle...
16140 
16141 		background color
16142 		font
16143 		border color and style
16144 
16145 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
16146 		please note that many widgets and in some modes will completely ignore properties as they will.
16147 		they are just hints you set, not promises.
16148 
16149 
16150 
16151 
16152 
16153 	So generally the existing virtual functions are just the default for the class. But individual objects
16154 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
16155 +/
16156 
16157 /++
16158 	Structure to represent a collection of background hints. New features can be added here, so make sure you use the provided constructors and factories for maximum compatibility.
16159 
16160 	History:
16161 		Added May 24, 2021.
16162 +/
16163 struct WidgetBackground {
16164 	/++
16165 		A background with the given solid color.
16166 	+/
16167 	this(Color color) {
16168 		this.color = color;
16169 	}
16170 
16171 	this(WidgetBackground bg) {
16172 		this = bg;
16173 	}
16174 
16175 	/++
16176 		Creates a widget from the string.
16177 
16178 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
16179 	+/
16180 	static WidgetBackground fromString(string s) {
16181 		return WidgetBackground(Color.fromString(s));
16182 	}
16183 
16184 	/++
16185 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
16186 
16187 		History:
16188 			Made `public` on December 18, 2022 (dub v10.10).
16189 	+/
16190 	Color color;
16191 }
16192 
16193 /++
16194 	Interface to a custom visual theme which is able to access and use style hint properties, draw stylistic elements, and even completely override existing class' paint methods (though I'd note that can be a lot harder than it may seem due to the various little details of state you need to reflect visually, so that should be your last result!)
16195 
16196 	Please note that this is only guaranteed to be used by custom widgets, and custom widgets are generally inferior to system widgets. Layout properties may be used by sytstem widgets though.
16197 
16198 	You should not inherit from this directly, but instead use [VisualTheme].
16199 
16200 	History:
16201 		Added May 8, 2021
16202 +/
16203 abstract class BaseVisualTheme {
16204 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
16205 	abstract void doPaint(Widget widget, WidgetPainter painter);
16206 
16207 	/+
16208 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
16209 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
16210 	+/
16211 
16212 	/++
16213 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
16214 		where the interpretation of the string varies for each property and may include things like measurement units.
16215 	+/
16216 	abstract string getPropertyString(Widget widget, string propertyName);
16217 
16218 	/++
16219 		Default background color of the window. Widgets also use this to simulate transparency.
16220 
16221 		Probably some shade of grey.
16222 	+/
16223 	abstract Color windowBackgroundColor();
16224 	abstract Color widgetBackgroundColor();
16225 	abstract Color foregroundColor();
16226 	abstract Color lightAccentColor();
16227 	abstract Color darkAccentColor();
16228 
16229 	/++
16230 		Colors used to indicate active selections in lists and text boxes, etc.
16231 	+/
16232 	abstract Color selectionForegroundColor();
16233 	/// ditto
16234 	abstract Color selectionBackgroundColor();
16235 
16236 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
16237 
16238 	/++
16239 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
16240 	+/
16241 	abstract OperatingSystemFont defaultFont(int dpi);
16242 
16243 	private OperatingSystemFont[int] defaultFontCache_;
16244 	private OperatingSystemFont defaultFontCached(int dpi) {
16245 		if(dpi !in defaultFontCache_) {
16246 			// FIXME: set this to false if X disconnect or if visual theme changes
16247 			defaultFontCache_[dpi] = defaultFont(dpi);
16248 		}
16249 		return defaultFontCache_[dpi];
16250 	}
16251 }
16252 
16253 /+
16254 	A widget should have:
16255 		classList
16256 		dataset
16257 		attributes
16258 		computedStyles
16259 		state (persistent)
16260 		dynamic state (focused, hover, etc)
16261 +/
16262 
16263 // visualTheme.computedStyle(this).paddingLeft
16264 
16265 
16266 /++
16267 	This is your entry point to create your own visual theme for custom widgets.
16268 
16269 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
16270 
16271 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
16272 +/
16273 abstract class VisualTheme(CRTP) : BaseVisualTheme {
16274 	override string getPropertyString(Widget widget, string propertyName) {
16275 		return null;
16276 	}
16277 
16278 	/+
16279 		mixin StyleOverride!Widget
16280 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
16281 		w.useStyleProperties(dg);
16282 	}
16283 	+/
16284 
16285 	final override void doPaint(Widget widget, WidgetPainter painter) {
16286 		auto derived = cast(CRTP) cast(void*) this;
16287 
16288 		scope void delegate(Widget, WidgetPainter) bestMatch;
16289 		int bestMatchScore;
16290 
16291 		static if(__traits(hasMember, CRTP, "paint"))
16292 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
16293 			static if(is(typeof(overload) Params == __parameters)) {
16294 				static assert(Params.length == 2);
16295 				static assert(is(Params[0] : Widget));
16296 				static assert(is(Params[1] == WidgetPainter));
16297 				static assert(is(typeof(&__traits(child, derived, overload)) == delegate), "Found a paint method that doesn't appear to be a delegate. One cause of this can be your dmd being too old, make sure it is version 2.094 or newer to use this feature."); // , __traits(getLocation, overload).stringof ~ " is not a delegate " ~ typeof(&__traits(child, derived, overload)).stringof);
16298 
16299 				alias type = Params[0];
16300 				if(cast(type) widget) {
16301 					auto score = baseClassCount!type;
16302 
16303 					if(score > bestMatchScore) {
16304 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
16305 						bestMatchScore = score;
16306 					}
16307 				}
16308 			} else static assert(0, "paint should be a method.");
16309 		}
16310 
16311 		if(bestMatch)
16312 			bestMatch(widget, painter);
16313 		else
16314 			widget.paint(painter);
16315 	}
16316 
16317 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
16318 
16319 	// I have to put these here even though I kinda don't want to since dmd regressed on detecting unimplemented interface functions through abstract classes
16320 	// mixin Beautiful95Theme;
16321 	mixin DefaultLightTheme;
16322 
16323 	private static struct Cached {
16324 		// i prolly want to do this
16325 	}
16326 }
16327 
16328 /// ditto
16329 mixin template Beautiful95Theme() {
16330 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
16331 	override Color widgetBackgroundColor() { return Color.white; }
16332 	override Color foregroundColor() { return Color.black; }
16333 	override Color darkAccentColor() { return Color(172, 172, 172); }
16334 	override Color lightAccentColor() { return Color(223, 223, 223); }
16335 	override Color selectionForegroundColor() { return Color.white; }
16336 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
16337 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
16338 }
16339 
16340 /// ditto
16341 mixin template DefaultLightTheme() {
16342 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
16343 	override Color widgetBackgroundColor() { return Color.white; }
16344 	override Color foregroundColor() { return Color.black; }
16345 	override Color darkAccentColor() { return Color(172, 172, 172); }
16346 	override Color lightAccentColor() { return Color(223, 223, 223); }
16347 	override Color selectionForegroundColor() { return Color.white; }
16348 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
16349 	override OperatingSystemFont defaultFont(int dpi) {
16350 		version(Windows)
16351 			return new OperatingSystemFont("Segoe UI");
16352 		else static if(UsingSimpledisplayCocoa) {
16353 			return (new OperatingSystemFont()).loadDefault;
16354 		} else {
16355 			// FIXME: undo xft's scaling so we don't end up double scaled
16356 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
16357 		}
16358 	}
16359 }
16360 
16361 /// ditto
16362 mixin template DefaultDarkTheme() {
16363 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
16364 	override Color widgetBackgroundColor() { return Color.black; }
16365 	override Color foregroundColor() { return Color.white; }
16366 	override Color darkAccentColor() { return Color(20, 20, 20); }
16367 	override Color lightAccentColor() { return Color(80, 80, 80); }
16368 	override Color selectionForegroundColor() { return Color.white; }
16369 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
16370 	override OperatingSystemFont defaultFont(int dpi) {
16371 		version(Windows)
16372 			return new OperatingSystemFont("Segoe UI", 12);
16373 		else static if(UsingSimpledisplayCocoa) {
16374 			return (new OperatingSystemFont()).loadDefault;
16375 		} else {
16376 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
16377 		}
16378 	}
16379 }
16380 
16381 /// ditto
16382 alias DefaultTheme = DefaultLightTheme;
16383 
16384 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
16385 	/+
16386 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
16387 	Color windowBackgroundColor() { return Color(242, 242, 242); }
16388 	Color darkAccentColor() { return windowBackgroundColor; }
16389 	Color lightAccentColor() { return windowBackgroundColor; }
16390 	+/
16391 }
16392 
16393 /++
16394 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
16395 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
16396 
16397 	History:
16398 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16399 +/
16400 class StateChanged(alias field) : Event {
16401 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
16402 	override bool cancelable() const { return false; }
16403 	this(Widget target, typeof(field) newValue) {
16404 		this.newValue = newValue;
16405 		super(EventString, target);
16406 	}
16407 
16408 	typeof(field) newValue;
16409 }
16410 
16411 /++
16412 	Convenience function to add a `triggered` event listener.
16413 
16414 	Its implementation is simply `w.addEventListener("triggered", dg);`
16415 
16416 	History:
16417 		Added November 27, 2021 (dub v10.4)
16418 +/
16419 void addWhenTriggered(Widget w, void delegate() dg) {
16420 	w.addEventListener("triggered", dg);
16421 }
16422 
16423 /++
16424 	Observable varables can be added to widgets and when they are changed, it fires
16425 	off a [StateChanged] event so you can react to it.
16426 
16427 	It is implemented as a getter and setter property, along with another helper you
16428 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
16429 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
16430 	example.
16431 
16432 	History:
16433 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16434 +/
16435 mixin template Observable(T, string name) {
16436 	private T backing;
16437 
16438 	mixin(q{
16439 		void } ~ name ~ q{_changed (void delegate(T) dg) {
16440 			this.addEventListener((StateChanged!this_thing ev) {
16441 				dg(ev.newValue);
16442 			});
16443 		}
16444 
16445 		@property T } ~ name ~ q{ () {
16446 			return backing;
16447 		}
16448 
16449 		@property void } ~ name ~ q{ (T t) {
16450 			backing = t;
16451 			auto event = new StateChanged!this_thing(this, t);
16452 			event.dispatch();
16453 		}
16454 	});
16455 
16456 	mixin("private alias this_thing = " ~ name ~ ";");
16457 }
16458 
16459 
16460 private bool startsWith(string test, string thing) {
16461 	if(test.length < thing.length)
16462 		return false;
16463 	return test[0 .. thing.length] == thing;
16464 }
16465 
16466 private bool endsWith(string test, string thing) {
16467 	if(test.length < thing.length)
16468 		return false;
16469 	return test[$ - thing.length .. $] == thing;
16470 }
16471 
16472 // still do layout delegation
16473 // and... split off Window from Widget.
16474 
16475 version(minigui_screenshots)
16476 struct Screenshot {
16477 	string name;
16478 }
16479 
16480 version(minigui_screenshots)
16481 static if(__VERSION__ > 2092)
16482 mixin(q{
16483 shared static this() {
16484 	import core.runtime;
16485 
16486 	static UnitTestResult screenshotMagic() {
16487 		string name;
16488 
16489 		import arsd.png;
16490 
16491 		auto results = new Window();
16492 		auto button = new Button("do it", results);
16493 
16494 		Window.newWindowCreated = delegate(Window w) {
16495 			Timer timer;
16496 			timer = new Timer(250, {
16497 				auto img = w.win.takeScreenshot();
16498 				timer.destroy();
16499 
16500 				version(Windows)
16501 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
16502 				else
16503 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
16504 
16505 				w.close();
16506 			});
16507 		};
16508 
16509 		button.addWhenTriggered( {
16510 
16511 		foreach(test; __traits(getUnitTests, mixin(__MODULE__))) {
16512 			name = null;
16513 			static foreach(attr; __traits(getAttributes, test)) {
16514 				static if(is(typeof(attr) == Screenshot))
16515 					name = attr.name;
16516 			}
16517 			if(name.length) {
16518 				test();
16519 			}
16520 		}
16521 
16522 		});
16523 
16524 		results.loop();
16525 
16526 		return UnitTestResult(0, 0, false, false);
16527 	}
16528 
16529 
16530 	Runtime.extendedModuleUnitTester = &screenshotMagic;
16531 }
16532 });
16533 version(minigui_screenshots) {
16534 	version(unittest)
16535 		void main() {}
16536 	else static assert(0, "dont forget the -unittest flag to dmd");
16537 }
16538 
16539 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
16540 // FIXME: make multiple accelerators disambiguate based ona rgs
16541 // FIXME: MainWindow ctor should have same arg order as Window
16542 // FIXME: mainwindow ctor w/ client area size instead of total size.
16543 // Push on/off button (basically an alternate display of a checkbox) -- BS_PUSHLIKE and maybe BS_TEXT (BS_TOP moves it). see also BS_FLAT.
16544 // FIXME: tri-state checkbox
16545 // FIXME: subordinate controls grouping...