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:5.0`, for example, because otherwise you'll get a
136 	console and other visual bugs.
137 
138 	HTML_To_Classes:
139 	$(SMALL_TABLE
140 		HTML Code | Minigui Class
141 
142 		`<input type="text">` | [LineEdit]
143 		`<textarea>` | [TextEdit]
144 		`<select>` | [DropDownSelection]
145 		`<input type="checkbox">` | [Checkbox]
146 		`<input type="radio">` | [Radiobox]
147 		`<button>` | [Button]
148 	)
149 
150 
151 	Stretchiness:
152 		The default is 4. You can use larger numbers for things that should
153 		consume a lot of space, and lower numbers for ones that are better at
154 		smaller sizes.
155 
156 	Overlapped_input:
157 		COMING EVENTUALLY:
158 		minigui will include a little bit of I/O functionality that just works
159 		with the event loop. If you want to get fancy, I suggest spinning up
160 		another thread and posting events back and forth.
161 
162 	$(H2 Add ons)
163 		See the `minigui_addons` directory in the arsd repo for some add on widgets
164 		you can import separately too.
165 
166 	$(H3 XML definitions)
167 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
168 
169 	$(H3 Scriptability)
170 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
171 		in this documentation, it means you can call it from the script language.
172 
173 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
174 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
175 
176 		---
177 		import arsd.minigui_xml;
178 		import arsd.script;
179 
180 		var globals = var.emptyObject;
181 		globals.makeWidgetFromString = &makeWidgetFromString;
182 
183 		// this now works
184 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
185 		---
186 
187 		More to come.
188 
189 	History:
190 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
191 
192 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
193 		tag this as version 2.0.
194 
195 		Among the changes:
196 		$(LIST
197 			* 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.
198 
199 			See [Event] for details.
200 
201 			* 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.
202 
203 			See [DoubleClickEvent] for details.
204 
205 			* 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.
206 
207 			See [Widget.Style] for details.
208 
209 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
210 
211 			* 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.
212 
213 			* 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.
214 
215 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
216 
217 			* 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.
218 
219 			* Various non-breaking additions.
220 		)
221 +/
222 module arsd.minigui;
223 
224 /++
225 	This hello world sample will have an oversized button, but that's ok, you see your first window!
226 +/
227 version(Demo)
228 unittest {
229 	import arsd.minigui;
230 
231 	void main() {
232 		auto window = new MainWindow();
233 
234 		// note the parent widget is almost always passed as the last argument to a constructor
235 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
236 		auto button = new Button("Close", window);
237 		button.addWhenTriggered({
238 			window.close();
239 		});
240 
241 		window.loop();
242 	}
243 
244 	main(); // exclude from docs
245 }
246 
247 /++
248 	This example shows one way you can partition your window into a header
249 	and sidebar. Here, the header and sidebar have a fixed width, while the
250 	rest of the content sizes with the window.
251 
252 	It might be a new way of thinking about window layout to do things this
253 	way - perhaps [GridLayout] more matches your style of thought - but the
254 	concept here is to partition the window into sub-boxes with a particular
255 	size, then partition those boxes into further boxes.
256 
257 	$(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.)
258 
259 	So to make the header, start with a child layout that has a max height.
260 	It will use that space from the top, then the remaining children will
261 	split the remaining area, meaning you can think of is as just being another
262 	box you can split again. Keep splitting until you have the look you desire.
263 +/
264 // https://github.com/adamdruppe/arsd/issues/310
265 version(minigui_screenshots)
266 @Screenshot("layout")
267 unittest {
268 	import arsd.minigui;
269 
270 	// This helper class is just to help make the layout boxes visible.
271 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
272 	class ColorWidget : Widget {
273 		this(Color color, Widget parent) {
274 			this.color = color;
275 			super(parent);
276 		}
277 		Color color;
278 		class Style : Widget.Style {
279 			override WidgetBackground background() { return WidgetBackground(color); }
280 		}
281 		mixin OverrideStyle!Style;
282 	}
283 
284 	void main() {
285 		auto window = new Window;
286 
287 		// the key is to give it a max height. This is one way to do it:
288 		auto header = new class HorizontalLayout {
289 			this() { super(window); }
290 			override int maxHeight() { return 50; }
291 		};
292 		// this next line is a shortcut way of doing it too, but it only works
293 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
294 		// is good to know how to make a new class like above anyway.
295 		// auto header = new HorizontalLayout(50, window);
296 
297 		auto bar = new HorizontalLayout(window);
298 
299 		// or since this is so common, VerticalLayout and HorizontalLayout both
300 		// can just take an argument in their constructor for max width/height respectively
301 
302 		// (could have tone this above too, but I wanted to demo both techniques)
303 		auto left = new VerticalLayout(100, bar);
304 
305 		// and this is the main section's container. A plain Widget instance is good enough here.
306 		auto container = new Widget(bar);
307 
308 		// and these just add color to the containers we made above for the screenshot.
309 		// in a real application, you can just add your actual controls instead of these.
310 		auto headerColorBox = new ColorWidget(Color.teal, header);
311 		auto leftColorBox = new ColorWidget(Color.green, left);
312 		auto rightColorBox = new ColorWidget(Color.purple, container);
313 
314 		window.loop();
315 	}
316 
317 	main(); // exclude from docs
318 }
319 
320 
321 import arsd.core;
322 public import arsd.simpledisplay;
323 /++
324 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
325 
326 	History:
327 		Was private until May 15, 2021.
328 +/
329 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
330 
331 version(Windows) {
332 	import core.sys.windows.winnls;
333 	import core.sys.windows.windef;
334 	import core.sys.windows.basetyps;
335 	import core.sys.windows.winbase;
336 	import core.sys.windows.winuser;
337 	import core.sys.windows.wingdi;
338 	static import gdi = core.sys.windows.wingdi;
339 }
340 
341 version(Windows) {
342 	version(minigui_manifest) {} else version=minigui_no_manifest;
343 
344 	version(minigui_no_manifest) {} else
345 	static if(__VERSION__ >= 2_083)
346 	version(CRuntime_Microsoft) { // FIXME: mingw?
347 		// assume we want commctrl6 whenever possible since there's really no reason not to
348 		// and this avoids some of the manifest hassle
349 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
350 	}
351 }
352 
353 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
354 private bool lastDefaultPrevented;
355 
356 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
357 alias scriptable = arsd_jsvar_compatible;
358 
359 version(Windows) {
360 	// use native widgets when available unless specifically asked otherwise
361 	version(custom_widgets) {
362 		enum bool UsingCustomWidgets = true;
363 		enum bool UsingWin32Widgets = false;
364 	} else {
365 		version = win32_widgets;
366 		enum bool UsingCustomWidgets = false;
367 		enum bool UsingWin32Widgets = true;
368 
369 		// give access to my text system for the rich text cross platform stuff
370 		version = use_new_text_system;
371 		import arsd.textlayouter;
372 	}
373 	// and native theming when needed
374 	//version = win32_theming;
375 } else {
376 	enum bool UsingCustomWidgets = true;
377 	enum bool UsingWin32Widgets = false;
378 	version=custom_widgets;
379 }
380 
381 
382 
383 /*
384 
385 	The main goals of minigui.d are to:
386 		1) Provide basic widgets that just work in a lightweight lib.
387 		   I basically want things comparable to a plain HTML form,
388 		   plus the easy and obvious things you expect from Windows
389 		   apps like a menu.
390 		2) Use native things when possible for best functionality with
391 		   least library weight.
392 		3) Give building blocks to provide easy extension for your
393 		   custom widgets, or hooking into additional native widgets
394 		   I didn't wrap.
395 		4) Provide interfaces for easy interaction between third
396 		   party minigui extensions. (event model, perhaps
397 		   signals/slots, drop-in ease of use bits.)
398 		5) Zero non-system dependencies, including Phobos as much as
399 		   I reasonably can. It must only import arsd.color and
400 		   my simpledisplay.d. If you need more, it will have to be
401 		   an extension module.
402 		6) An easy layout system that generally works.
403 
404 	A stretch goal is to make it easy to make gui forms with code,
405 	some kind of resource file (xml?) and even a wysiwyg designer.
406 
407 	Another stretch goal is to make it easy to hook data into the gui,
408 	including from reflection. So like auto-generate a form from a
409 	function signature or struct definition, or show a list from an
410 	array that automatically updates as the array is changed. Then,
411 	your program focuses on the data more than the gui interaction.
412 
413 
414 
415 	STILL NEEDED:
416 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
417 		* slider
418 		* listbox
419 		* spinner
420 		* label?
421 		* rich text
422 */
423 
424 
425 /+
426 	enum LayoutMethods {
427 		 verticalFlex,
428 		 horizontalFlex,
429 		 inlineBlock, // left to right, no stretch, goes to next line as needed
430 		 static, // just set to x, y
431 		 verticalNoStretch, // browser style default
432 
433 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
434 
435 		 grid, // magic
436 	}
437 +/
438 
439 /++
440 	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.
441 
442 
443 	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.
444 
445 	---
446 	class MinimalWidget : Widget {
447 		this(Widget parent) {
448 			super(parent);
449 		}
450 	}
451 	---
452 
453 	$(SIDEBAR
454 		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.
455 	)
456 
457 	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.
458 
459 	Among the things you'll most likely want to change in your custom widget:
460 
461 	$(LIST
462 		* 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.)
463 
464 		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.
465 
466 		Do this $(I after) calling the `super` constructor.
467 
468 		* 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.
469 
470 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
471 
472 		* 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.
473 
474 		* 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.
475 	)
476 
477 	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.
478 
479 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
480 
481 	Your own custom-drawn and native system controls can exist side-by-side.
482 
483 	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.
484 +/
485 class Widget : ReflectableProperties {
486 
487 	private bool willDraw() {
488 		return true;
489 	}
490 
491 	/+
492 	/++
493 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
494 
495 		History:
496 			Added September 15, 2021
497 			implemented.... ???
498 	+/
499 	void prepareReflection(this This)() {
500 
501 	}
502 	+/
503 
504 	private bool _enabled = true;
505 
506 	/++
507 		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.
508 
509 		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.
510 
511 		History:
512 			Added November 23, 2021 (dub v10.4)
513 
514 			Warning: the specific behavior of disabling with parents may change in the future.
515 		Bugs:
516 			Currently only implemented for widgets backed by native Windows controls.
517 
518 		See_Also: [disabledReason], [disabledBy]
519 	+/
520 	@property bool enabled() {
521 		return disabledBy() is null;
522 	}
523 
524 	/// ditto
525 	@property void enabled(bool yes) {
526 		_enabled = yes;
527 		version(win32_widgets) {
528 			if(hwnd)
529 				EnableWindow(hwnd, yes);
530 		}
531 		setDynamicState(DynamicState.disabled, yes);
532 	}
533 
534 	private string disabledReason_;
535 
536 	/++
537 		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.
538 
539 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
540 
541 		History:
542 			Added November 23, 2021 (dub v10.4)
543 		See_Also: [enabled], [disabledBy]
544 	+/
545 	@property string disabledReason() {
546 		auto w = disabledBy();
547 		return (w is null) ? null : w.disabledReason_;
548 	}
549 
550 	/// ditto
551 	@property void disabledReason(string reason) {
552 		disabledReason_ = reason;
553 	}
554 
555 	/++
556 		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.
557 
558 		History:
559 			Added November 25, 2021 (dub v10.4)
560 		See_Also: [enabled], [disabledReason]
561 	+/
562 	Widget disabledBy() {
563 		Widget p = this;
564 		while(p) {
565 			if(!p._enabled)
566 				return p;
567 			p = p.parent;
568 		}
569 		return null;
570 	}
571 
572 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
573 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
574 		if(valueIsJson)
575 			return SetPropertyResult.wrongFormat;
576 		switch(name) {
577 			case "name":
578 				this.name = value.idup;
579 				return SetPropertyResult.success;
580 			case "statusTip":
581 				this.statusTip = value.idup;
582 				return SetPropertyResult.success;
583 			default:
584 				return SetPropertyResult.noSuchProperty;
585 		}
586 	}
587 	/// ditto
588 	void getPropertiesList(scope void delegate(string name) sink) const {
589 		sink("name");
590 		sink("statusTip");
591 	}
592 	/// ditto
593 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
594 		switch(name) {
595 			case "name":
596 				sink(name, this.name, false);
597 				return;
598 			case "statusTip":
599 				sink(name, this.statusTip, false);
600 				return;
601 			default:
602 				sink(name, null, true);
603 		}
604 	}
605 
606 	/++
607 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
608 
609 		History:
610 			Added November 25, 2021 (dub v10.5)
611 			`Point` overload added January 12, 2022 (dub v10.6)
612 	+/
613 	int scaleWithDpi(int value, int assumedDpi = 96) {
614 		// avoid potential overflow with common special values
615 		if(value == int.max)
616 			return int.max;
617 		if(value == int.min)
618 			return int.min;
619 		if(value == 0)
620 			return 0;
621 		return value * currentDpi(assumedDpi) / assumedDpi;
622 	}
623 
624 	/// ditto
625 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
626 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
627 	}
628 
629 	/++
630 		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.
631 
632 		Not entirely stable.
633 
634 		History:
635 			Added August 25, 2023 (dub v11.1)
636 	+/
637 	final int currentDpi(int assumedDpi = 96) {
638 		// assert(parentWindow !is null);
639 		// assert(parentWindow.win !is null);
640 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
641 		//divide = 138; // to test 1.5x
642 		// 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.
643 		// this also covers the case when actualDpi returns 0.
644 		if(divide < 96)
645 			divide = 96;
646 		return divide;
647 	}
648 
649 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
650 	// I'll think up something better eventually
651 
652 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
653 	protected final int defaultLineHeight() {
654 		auto cs = getComputedStyle();
655 		if(cs.font && !cs.font.isNull)
656 			return cs.font.height() * 5 / 4;
657 		else
658 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
659 	}
660 
661 	/++
662 
663 		History:
664 			Added August 25, 2023 (dub v11.1)
665 	+/
666 	protected final int defaultTextHeight(int numberOfLines = 1) {
667 		auto cs = getComputedStyle();
668 		if(cs.font && !cs.font.isNull)
669 			return cs.font.height() * numberOfLines;
670 		else
671 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
672 	}
673 
674 	protected final int defaultTextWidth(const(char)[] text) {
675 		auto cs = getComputedStyle();
676 		if(cs.font && !cs.font.isNull)
677 			return cs.font.stringWidth(text);
678 		else
679 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
680 	}
681 
682 	/++
683 		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.
684 
685 		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.
686 
687 		History:
688 			Added May 22, 2021
689 	+/
690 	protected bool encapsulatedChildren() {
691 		return false;
692 	}
693 
694 	private void privateDpiChanged() {
695 		dpiChanged();
696 		foreach(child; children)
697 			child.privateDpiChanged();
698 	}
699 
700 	/++
701 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
702 
703 		History:
704 			Added January 12, 2022 (dub v10.6)
705 	+/
706 	protected void dpiChanged() {
707 
708 	}
709 
710 	// Default layout properties {
711 
712 		int minWidth() { return 0; }
713 		int minHeight() {
714 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
715 			int sum = this.paddingTop + this.paddingBottom;
716 			foreach(child; children) {
717 				if(child.hidden)
718 					continue;
719 				sum += child.minHeight();
720 				sum += child.marginTop();
721 				sum += child.marginBottom();
722 			}
723 
724 			return sum;
725 		}
726 		int maxWidth() { return int.max; }
727 		int maxHeight() { return int.max; }
728 		int widthStretchiness() { return 4; }
729 		int heightStretchiness() { return 4; }
730 
731 		/++
732 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
733 
734 			History:
735 				Added June 15, 2021 (dub v10.1)
736 		+/
737 		int widthShrinkiness() { return 0; }
738 		/// ditto
739 		int heightShrinkiness() { return 0; }
740 
741 		/++
742 			The initial size of the widget for layout calculations. Default is 0.
743 
744 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
745 
746 			History:
747 				Added June 15, 2021 (dub v10.1)
748 		+/
749 		int flexBasisWidth() { return 0; }
750 		/// ditto
751 		int flexBasisHeight() { return 0; }
752 
753 		/++
754 			Not stable.
755 
756 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
757 
758 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
759 
760 			History:
761 				Added January 5, 2023
762 		+/
763 		Rectangle defaultMargin;
764 		/// ditto
765 		Rectangle defaultPadding;
766 
767 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
768 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
769 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
770 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
771 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
772 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
773 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
774 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
775 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
776 
777 		private bool recomputeChildLayoutRequired = true;
778 		private static class RecomputeEvent {}
779 		private __gshared rce = new RecomputeEvent();
780 		protected final void queueRecomputeChildLayout() {
781 			recomputeChildLayoutRequired = true;
782 
783 			if(this.parentWindow) {
784 				auto sw = this.parentWindow.win;
785 				assert(sw !is null);
786 				if(!sw.eventQueued!RecomputeEvent) {
787 					sw.postEvent(rce);
788 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
789 				}
790 			}
791 
792 		}
793 
794 		protected final void recomputeChildLayoutEntry() {
795 			if(recomputeChildLayoutRequired) {
796 				recomputeChildLayout();
797 				recomputeChildLayoutRequired = false;
798 				redraw();
799 			} else {
800 				// I still need to check the tree just in case one of them was queued up
801 				// and the event came up here instead of there.
802 				foreach(child; children)
803 					child.recomputeChildLayoutEntry();
804 			}
805 		}
806 
807 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
808 		void recomputeChildLayout() {
809 			.recomputeChildLayout!"height"(this);
810 		}
811 
812 	// }
813 
814 
815 	/++
816 		Returns the style's tag name string this object uses.
817 
818 		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.
819 
820 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
821 
822 		History:
823 			Added May 10, 2021
824 	+/
825 	string styleTagName() const {
826 		string n = typeid(this).name;
827 		foreach_reverse(idx, ch; n)
828 			if(ch == '.') {
829 				n = n[idx + 1 .. $];
830 				break;
831 			}
832 		return n;
833 	}
834 
835 	/// API for the [styleClassList]
836 	static struct ClassList {
837 		private Widget widget;
838 
839 		///
840 		void add(string s) {
841 			widget.styleClassList_ ~= s;
842 		}
843 
844 		///
845 		void remove(string s) {
846 			foreach(idx, s1; widget.styleClassList_)
847 				if(s1 == s) {
848 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
849 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
850 					widget.styleClassList_.assumeSafeAppend();
851 					return;
852 				}
853 		}
854 
855 		/// Returns true if it was added, false if it was removed.
856 		bool toggle(string s) {
857 			if(contains(s)) {
858 				remove(s);
859 				return false;
860 			} else {
861 				add(s);
862 				return true;
863 			}
864 		}
865 
866 		///
867 		bool contains(string s) const {
868 			foreach(s1; widget.styleClassList_)
869 				if(s1 == s)
870 					return true;
871 			return false;
872 
873 		}
874 	}
875 
876 	private string[] styleClassList_;
877 
878 	/++
879 		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.
880 
881 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
882 
883 		History:
884 			Added May 10, 2021
885 	+/
886 	inout(ClassList) styleClassList() inout {
887 		return cast(inout(ClassList)) ClassList(cast() this);
888 	}
889 
890 	/++
891 		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.
892 
893 		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.
894 
895 		The upper 32 bits are available for your own extensions.
896 
897 		History:
898 			Added May 10, 2021
899 	+/
900 	enum DynamicState : ulong {
901 		focus = (1 << 0), /// the widget currently has the keyboard focus
902 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
903 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!)
904 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!)
905 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
906 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
907 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
908 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
909 		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.
910 
911 		USER_BEGIN = (1UL << 32),
912 	}
913 
914 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
915 
916 	/// ditto
917 	@property ulong dynamicState() { return dynamicState_; }
918 	/// ditto
919 	@property ulong dynamicState(ulong newValue) {
920 		if(dynamicState != newValue) {
921 			auto old = dynamicState_;
922 			dynamicState_ = newValue;
923 
924 			useStyleProperties((scope Widget.Style s) {
925 				if(s.variesWithState(old ^ newValue))
926 					redraw();
927 			});
928 		}
929 		return dynamicState_;
930 	}
931 
932 	/// ditto
933 	void setDynamicState(ulong flags, bool state) {
934 		auto ds = dynamicState_;
935 		if(state)
936 			ds |= flags;
937 		else
938 			ds &= ~flags;
939 
940 		dynamicState = ds;
941 	}
942 
943 	private ulong dynamicState_;
944 
945 	deprecated("Use dynamic styles instead now") {
946 		Color backgroundColor() { return backgroundColor_; }
947 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
948 
949 		MouseCursor cursor() { return GenericCursor.Default; }
950 	} private Color backgroundColor_ = Color.transparent;
951 
952 
953 	/++
954 		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).
955 
956 		It is here so there can be a specificity switch.
957 
958 		See [OverrideStyle] for a helper function to use your own.
959 
960 		History:
961 			Added May 11, 2021
962 	+/
963 	static class Style/* : StyleProperties*/ {
964 		public Widget widget; // public because the mixin template needs access to it
965 
966 		/++
967 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
968 
969 			History:
970 				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.
971 		+/
972 		bool variesWithState(ulong dynamicStateFlags) {
973 			version(win32_widgets) {
974 				if(widget.hwnd)
975 					return false;
976 			}
977 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
978 		}
979 
980 		///
981 		Color foregroundColor() {
982 			return WidgetPainter.visualTheme.foregroundColor;
983 		}
984 
985 		///
986 		WidgetBackground background() {
987 			// the default is a "transparent" background, which means
988 			// it goes as far up as it can to get the color
989 			if (widget.backgroundColor_ != Color.transparent)
990 				return WidgetBackground(widget.backgroundColor_);
991 			if (widget.parent)
992 				return widget.parent.getComputedStyle.background;
993 			return WidgetBackground(widget.backgroundColor_);
994 		}
995 
996 		private static OperatingSystemFont fontCached_;
997 		private OperatingSystemFont fontCached() {
998 			if(fontCached_ is null)
999 				fontCached_ = font();
1000 			return fontCached_;
1001 		}
1002 
1003 		/++
1004 			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.
1005 		+/
1006 		OperatingSystemFont font() {
1007 			return null;
1008 		}
1009 
1010 		/++
1011 			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.
1012 
1013 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1014 
1015 			History:
1016 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1017 		+/
1018 		MouseCursor cursor() {
1019 			return GenericCursor.Default;
1020 		}
1021 
1022 		FrameStyle borderStyle() {
1023 			return FrameStyle.none;
1024 		}
1025 
1026 		/++
1027 		+/
1028 		Color borderColor() {
1029 			return Color.transparent;
1030 		}
1031 
1032 		FrameStyle outlineStyle() {
1033 			if(widget.dynamicState & DynamicState.focus)
1034 				return FrameStyle.dotted;
1035 			else
1036 				return FrameStyle.none;
1037 		}
1038 
1039 		Color outlineColor() {
1040 			return foregroundColor;
1041 		}
1042 	}
1043 
1044 	/++
1045 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1046 		The basic usage is simple:
1047 
1048 		---
1049 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1050 			// override style hints as-needed here
1051 		}
1052 		OverrideStyle!Style; // add the method
1053 		---
1054 
1055 		$(TIP
1056 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1057 			can not be inherited by other objects whereas the static one can. A property on the base class,
1058 			called [Widget.Style.widget|widget], is available for you to access its properties.
1059 		)
1060 
1061 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1062 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1063 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1064 
1065 
1066 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1067 		You may also just override `variesWithState` when you use this flag.
1068 
1069 		---
1070 		mixin OverrideStyle!(
1071 			DynamicState.focus, YourFocusedStyle,
1072 			DynamicState.hover, YourHoverStyle,
1073 			YourDefaultStyle
1074 		)
1075 		---
1076 
1077 		It checks if `dynamicState` matches the state and if so, returns the object given.
1078 
1079 		If there is no state mask given, the next one matches everything. The first match given is used.
1080 
1081 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1082 		find much use for this whole-class swap out.
1083 
1084 		History:
1085 			Added May 16, 2021
1086 	+/
1087 	static protected mixin template OverrideStyle(S...) {
1088 		static import amg = arsd.minigui;
1089 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1090 			ulong mask = 0;
1091 			foreach(idx, thing; S) {
1092 				static if(is(typeof(thing) : ulong)) {
1093 					mask = thing;
1094 				} else {
1095 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1096 						//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.");
1097 						scope amg.Widget.Style s = new thing();
1098 						s.widget = this;
1099 						dg(s);
1100 						return;
1101 					}
1102 				}
1103 			}
1104 		}
1105 	}
1106 	/++
1107 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1108 	+/
1109 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1110 		scope Style s = new Style();
1111 		s.widget = this;
1112 		dg(s);
1113 	}
1114 
1115 
1116 	protected void sendResizeEvent() {
1117 		this.emit!ResizeEvent();
1118 	}
1119 
1120 	Menu contextMenu(int x, int y) { return null; }
1121 
1122 	final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) {
1123 		if(parentWindow is null || parentWindow.win is null) return false;
1124 
1125 		auto menu = this.contextMenu(x, y);
1126 		if(menu is null)
1127 			return false;
1128 
1129 		version(win32_widgets) {
1130 			// FIXME: if it is -1, -1, do it at the current selection location instead
1131 			// tho the corner of the window, whcih it does now, isn't the literal worst.
1132 
1133 			if(screenX < 0 && screenY < 0) {
1134 				auto p = this.globalCoordinates();
1135 				if(screenX == -2)
1136 					p.x += x;
1137 				if(screenY == -2)
1138 					p.y += y;
1139 
1140 				screenX = p.x;
1141 				screenY = p.y;
1142 			}
1143 
1144 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1145 				throw new Exception("TrackContextMenuEx");
1146 		} else version(custom_widgets) {
1147 			menu.popup(this, x, y);
1148 		}
1149 
1150 		return true;
1151 	}
1152 
1153 	/++
1154 		Removes this widget from its parent.
1155 
1156 		History:
1157 			`removeWidget` was made `final` on May 11, 2021.
1158 	+/
1159 	@scriptable
1160 	final void removeWidget() {
1161 		auto p = this.parent;
1162 		if(p) {
1163 			int item;
1164 			for(item = 0; item < p._children.length; item++)
1165 				if(p._children[item] is this)
1166 					break;
1167 			auto idx = item;
1168 			for(; item < p._children.length - 1; item++)
1169 				p._children[item] = p._children[item + 1];
1170 			p._children = p._children[0 .. $-1];
1171 
1172 			this.parent.widgetRemoved(idx, this);
1173 			//this.parent = null;
1174 
1175 			p.queueRecomputeChildLayout();
1176 		}
1177 		version(win32_widgets) {
1178 			removeAllChildren();
1179 			if(hwnd) {
1180 				DestroyWindow(hwnd);
1181 				hwnd = null;
1182 			}
1183 		}
1184 	}
1185 
1186 	/++
1187 		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.
1188 
1189 		History:
1190 			Added September 19, 2021
1191 	+/
1192 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1193 
1194 	/++
1195 		Removes all child widgets from `this`. You should not use the removed widgets again.
1196 
1197 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1198 
1199 		History:
1200 			Added July 1, 2021 (dub v10.2)
1201 	+/
1202 	void removeAllChildren() {
1203 		version(win32_widgets)
1204 		foreach(child; _children) {
1205 			child.removeAllChildren();
1206 			if(child.hwnd) {
1207 				DestroyWindow(child.hwnd);
1208 				child.hwnd = null;
1209 			}
1210 		}
1211 		auto orig = this._children;
1212 		this._children = null;
1213 		foreach(idx, w; orig)
1214 			this.widgetRemoved(idx, w);
1215 
1216 		queueRecomputeChildLayout();
1217 	}
1218 
1219 	/++
1220 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1221 	+/
1222 	@scriptable
1223 	Widget getChildByName(string name) {
1224 		return getByName(name);
1225 	}
1226 	/++
1227 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1228 	+/
1229 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1230 		if(this.name == name)
1231 			if(auto c = cast(WidgetClass) this)
1232 				return c;
1233 		foreach(child; children) {
1234 			auto w = child.getByName(name);
1235 			if(auto c = cast(WidgetClass) w)
1236 				return c;
1237 		}
1238 		return null;
1239 	}
1240 
1241 	/++
1242 		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.
1243 		Names should be unique in a window.
1244 
1245 		See_Also: [getByName], [getChildByName]
1246 	+/
1247 	@scriptable string name;
1248 
1249 	private EventHandler[][string] bubblingEventHandlers;
1250 	private EventHandler[][string] capturingEventHandlers;
1251 
1252 	/++
1253 		Default event handlers. These are called on the appropriate
1254 		event unless [Event.preventDefault] is called on the event at
1255 		some point through the bubbling process.
1256 
1257 
1258 		If you are implementing your own widget and want to add custom
1259 		events, you should follow the same pattern here: create a virtual
1260 		function named `defaultEventHandler_eventname` with the implementation,
1261 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1262 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1263 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1264 		This ensures virtual dispatch based on the correct subclass.
1265 
1266 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1267 		overridden version.
1268 
1269 		You only need to do that on parent classes adding NEW event types. If you
1270 		just want to change the default behavior of an existing event type in a subclass,
1271 		you override the function (and optionally call `super.method_name`) like normal.
1272 
1273 	+/
1274 	protected EventHandler[string] defaultEventHandlers;
1275 
1276 	/// ditto
1277 	void setupDefaultEventHandlers() {
1278 		defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); };
1279 		defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); };
1280 		defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); };
1281 		defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); };
1282 		defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); };
1283 		defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); };
1284 		defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); };
1285 		defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); };
1286 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); };
1287 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); };
1288 		defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); };
1289 		defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); };
1290 		defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); };
1291 		defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); };
1292 		defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); };
1293 		defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); };
1294 		defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); };
1295 		defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); };
1296 	}
1297 
1298 	/// ditto
1299 	void defaultEventHandler_click(ClickEvent event) {}
1300 	/// ditto
1301 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1302 	/// ditto
1303 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1304 	/// ditto
1305 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1306 	/// ditto
1307 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1308 		if(event.button == MouseButton.left) {
1309 			if(this.tabStop)
1310 				this.focus();
1311 		}
1312 	}
1313 	/// ditto
1314 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1315 	/// ditto
1316 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1317 	/// ditto
1318 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1319 	/// ditto
1320 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1321 	/// ditto
1322 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1323 	/// ditto
1324 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1325 	/// ditto
1326 	void defaultEventHandler_char(CharEvent event) {}
1327 	/// ditto
1328 	void defaultEventHandler_triggered(Event event) {}
1329 	/// ditto
1330 	void defaultEventHandler_change(Event event) {}
1331 	/// ditto
1332 	void defaultEventHandler_focus(Event event) {}
1333 	/// ditto
1334 	void defaultEventHandler_blur(Event event) {}
1335 	/// ditto
1336 	void defaultEventHandler_focusin(Event event) {}
1337 	/// ditto
1338 	void defaultEventHandler_focusout(Event event) {}
1339 
1340 	/++
1341 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1342 
1343 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1344 
1345 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1346 		of participating in handler delegation.
1347 
1348 		$(TIP
1349 			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.
1350 		)
1351 	+/
1352 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1353 		return addEventListener(event, (Widget, scope Event e) {
1354 			if(e.srcElement is this)
1355 				handler();
1356 		}, useCapture);
1357 	}
1358 
1359 	/// ditto
1360 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1361 		return addEventListener(event, (Widget, Event e) {
1362 			if(e.srcElement is this)
1363 				handler(e);
1364 		}, useCapture);
1365 	}
1366 
1367 	/// ditto
1368 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1369 		static if(is(Handler Fn == delegate)) {
1370 		static if(is(Fn Params == __parameters)) {
1371 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1372 				if(e.srcElement !is this)
1373 					return;
1374 				auto ty = cast(Params[0]) e;
1375 				if(ty !is null)
1376 					handler(ty);
1377 			}, useCapture);
1378 		} else static assert(0);
1379 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1380 	}
1381 
1382 	/// ditto
1383 	@scriptable
1384 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1385 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1386 	}
1387 
1388 	/// ditto
1389 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1390 		static if(is(Handler Fn == delegate)) {
1391 		static if(is(Fn Params == __parameters)) {
1392 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1393 				auto ty = cast(Params[0]) e;
1394 				if(ty !is null)
1395 					handler(ty);
1396 			}, useCapture);
1397 		} else static assert(0);
1398 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1399 	}
1400 
1401 	/// ditto
1402 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1403 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1404 	}
1405 
1406 	/// ditto
1407 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1408 		if(event.length > 2 && event[0..2] == "on")
1409 			event = event[2 .. $];
1410 
1411 		if(useCapture)
1412 			capturingEventHandlers[event] ~= handler;
1413 		else
1414 			bubblingEventHandlers[event] ~= handler;
1415 
1416 		return EventListener(this, event, handler, useCapture);
1417 	}
1418 
1419 	/// ditto
1420 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1421 		if(event.length > 2 && event[0..2] == "on")
1422 			event = event[2 .. $];
1423 
1424 		if(useCapture) {
1425 			if(event in capturingEventHandlers)
1426 			foreach(ref evt; capturingEventHandlers[event])
1427 				if(evt is handler) evt = null;
1428 		} else {
1429 			if(event in bubblingEventHandlers)
1430 			foreach(ref evt; bubblingEventHandlers[event])
1431 				if(evt is handler) evt = null;
1432 		}
1433 	}
1434 
1435 	/// ditto
1436 	void removeEventListener(EventListener listener) {
1437 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1438 	}
1439 
1440 	static if(UsingSimpledisplayX11) {
1441 		void discardXConnectionState() {
1442 			foreach(child; children)
1443 				child.discardXConnectionState();
1444 		}
1445 
1446 		void recreateXConnectionState() {
1447 			foreach(child; children)
1448 				child.recreateXConnectionState();
1449 			redraw();
1450 		}
1451 	}
1452 
1453 	/++
1454 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1455 
1456 		History:
1457 			`globalCoordinates` was made `final` on May 11, 2021.
1458 	+/
1459 	Point globalCoordinates() {
1460 		int x = this.x;
1461 		int y = this.y;
1462 		auto p = this.parent;
1463 		while(p) {
1464 			x += p.x;
1465 			y += p.y;
1466 			p = p.parent;
1467 		}
1468 
1469 		static if(UsingSimpledisplayX11) {
1470 			auto dpy = XDisplayConnection.get;
1471 			arsd.simpledisplay.Window dummyw;
1472 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1473 		} else version(Windows) {
1474 			POINT pt;
1475 			pt.x = x;
1476 			pt.y = y;
1477 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1478 			x = pt.x;
1479 			y = pt.y;
1480 		} else {
1481 			featureNotImplemented();
1482 		}
1483 
1484 		return Point(x, y);
1485 	}
1486 
1487 	version(win32_widgets)
1488 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1489 
1490 	version(win32_widgets)
1491 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1492 	void handleWmCommand(ushort cmd, ushort id) {}
1493 
1494 	version(win32_widgets)
1495 	/++
1496 		Called when a WM_NOTIFY is sent to the associated hwnd.
1497 
1498 		History:
1499 	+/
1500 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1501 
1502 	version(win32_widgets)
1503 	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); }
1504 
1505 	/++
1506 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1507 
1508 		Updates to this variable will only be made visible on the next mouse enter event.
1509 	+/
1510 	@scriptable string statusTip;
1511 	// string toolTip;
1512 	// string helpText;
1513 
1514 	/++
1515 		If true, this widget can be focused via keyboard control with the tab key.
1516 
1517 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1518 	+/
1519 	bool tabStop = true;
1520 	/++
1521 		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.)
1522 	+/
1523 	int tabOrder;
1524 
1525 	version(win32_widgets) {
1526 		static Widget[HWND] nativeMapping;
1527 		/// The native handle, if there is one.
1528 		HWND hwnd;
1529 		WNDPROC originalWindowProcedure;
1530 
1531 		SimpleWindow simpleWindowWrappingHwnd;
1532 
1533 		// please note it IGNORES your return value and does NOT forward it to Windows!
1534 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1535 			return 0;
1536 		}
1537 	}
1538 	private bool implicitlyCreated;
1539 
1540 	/// 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.
1541 	int x;
1542 	/// ditto
1543 	int y;
1544 	private int _width;
1545 	private int _height;
1546 	private Widget[] _children;
1547 	private Widget _parent;
1548 	private Window _parentWindow;
1549 
1550 	/++
1551 		Returns the window to which this widget is attached.
1552 
1553 		History:
1554 			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.
1555 	+/
1556 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1557 	private @property void parentWindow(Window parent) {
1558 		_parentWindow = parent;
1559 		foreach(child; children)
1560 			child.parentWindow = parent; // please note that this is recursive
1561 	}
1562 
1563 	/++
1564 		Returns the list of the widget's children.
1565 
1566 		History:
1567 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1568 
1569 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1570 	+/
1571 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1572 
1573 	/++
1574 		Returns the widget's parent.
1575 
1576 		History:
1577 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1578 
1579 			The parent should only be managed by the [addChild] and [removeWidget] method.
1580 	+/
1581 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1582 
1583 	/// The widget's current size.
1584 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1585 	/// ditto
1586 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1587 
1588 	/// Only the layout manager should be calling these.
1589 	final protected @property int width(int a) @safe { return _width = a; }
1590 	/// ditto
1591 	final protected @property int height(int a) @safe { return _height = a; }
1592 
1593 	/++
1594 		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.
1595 
1596 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
1597 	+/
1598 	protected void registerMovement() {
1599 		version(win32_widgets) {
1600 			if(hwnd) {
1601 				auto pos = getChildPositionRelativeToParentHwnd(this);
1602 				MoveWindow(hwnd, pos[0], pos[1], width, height, false); // 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
1603 			}
1604 		}
1605 		sendResizeEvent();
1606 	}
1607 
1608 	/// Creates the widget and adds it to the parent.
1609 	this(Widget parent) {
1610 		if(parent !is null)
1611 			parent.addChild(this);
1612 		setupDefaultEventHandlers();
1613 	}
1614 
1615 	/// 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.
1616 	@scriptable
1617 	bool isFocused() {
1618 		return parentWindow && parentWindow.focusedWidget is this;
1619 	}
1620 
1621 	private bool showing_ = true;
1622 	///
1623 	bool showing() { return showing_; }
1624 	///
1625 	bool hidden() { return !showing_; }
1626 	/++
1627 		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.
1628 	+/
1629 	void showing(bool s, bool recalculate = true) {
1630 		auto so = showing_;
1631 		showing_ = s;
1632 		if(s != so) {
1633 			version(win32_widgets)
1634 			if(hwnd)
1635 				ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE);
1636 
1637 			if(parent && recalculate) {
1638 				parent.queueRecomputeChildLayout();
1639 				parent.redraw();
1640 			}
1641 
1642 			foreach(child; children)
1643 				child.showing(s, false);
1644 
1645 		}
1646 		queueRecomputeChildLayout();
1647 		redraw();
1648 	}
1649 	/// Convenience method for `showing = true`
1650 	@scriptable
1651 	void show() {
1652 		showing = true;
1653 	}
1654 	/// Convenience method for `showing = false`
1655 	@scriptable
1656 	void hide() {
1657 		showing = false;
1658 	}
1659 
1660 	///
1661 	@scriptable
1662 	void focus() {
1663 		assert(parentWindow !is null);
1664 		if(isFocused())
1665 			return;
1666 
1667 		if(parentWindow.focusedWidget) {
1668 			// FIXME: more details here? like from and to
1669 			auto from = parentWindow.focusedWidget;
1670 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
1671 			parentWindow.focusedWidget = null;
1672 			from.emit!BlurEvent();
1673 			this.emit!FocusOutEvent();
1674 		}
1675 
1676 
1677 		version(win32_widgets) {
1678 			if(this.hwnd !is null)
1679 				SetFocus(this.hwnd);
1680 		}
1681 		//else static if(UsingSimpledisplayX11)
1682 			//this.parentWindow.win.focus();
1683 
1684 		parentWindow.focusedWidget = this;
1685 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
1686 		this.emit!FocusEvent();
1687 		this.emit!FocusInEvent();
1688 	}
1689 
1690 	/+
1691 	/++
1692 		Unfocuses the widget. This may reset
1693 	+/
1694 	@scriptable
1695 	void blur() {
1696 
1697 	}
1698 	+/
1699 
1700 
1701 	/++
1702 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
1703 
1704 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
1705 	+/
1706 	void attachedToWindow(Window w) {}
1707 	/++
1708 		Callback when the widget is added to another widget.
1709 
1710 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
1711 	+/
1712 	void addedTo(Widget w) {}
1713 
1714 	/++
1715 		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.
1716 
1717 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
1718 	+/
1719 	protected void addChild(Widget w, int position = int.max) {
1720 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
1721 		assert(w !is this, "Child cannot be its own parent!");
1722 		w._parent = this;
1723 		if(position == int.max || position == children.length) {
1724 			_children ~= w;
1725 		} else {
1726 			assert(position < _children.length);
1727 			_children.length = _children.length + 1;
1728 			for(int i = cast(int) _children.length - 1; i > position; i--)
1729 				_children[i] = _children[i - 1];
1730 			_children[position] = w;
1731 		}
1732 
1733 		this.parentWindow = this._parentWindow;
1734 
1735 		w.addedTo(this);
1736 
1737 		if(this.hidden)
1738 			w.showing = false;
1739 
1740 		if(parentWindow !is null) {
1741 			w.attachedToWindow(parentWindow);
1742 			parentWindow.queueRecomputeChildLayout();
1743 			parentWindow.redraw();
1744 		}
1745 	}
1746 
1747 	/++
1748 		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.
1749 	+/
1750 	Widget getChildAtPosition(int x, int y) {
1751 		// it goes backward so the last one to show gets picked first
1752 		// might use z-index later
1753 		foreach_reverse(child; children) {
1754 			if(child.hidden)
1755 				continue;
1756 			if(child.x <= x && child.y <= y
1757 				&& ((x - child.x) < child.width)
1758 				&& ((y - child.y) < child.height))
1759 			{
1760 				return child;
1761 			}
1762 		}
1763 
1764 		return null;
1765 	}
1766 
1767 	/++
1768 		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.
1769 
1770 		History:
1771 			Added July 2, 2021 (v10.2)
1772 	+/
1773 	protected void addScrollPosition(ref int x, ref int y) {};
1774 
1775 	/++
1776 		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.
1777 
1778 		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.
1779 
1780 		[paint] is not called for system widgets as the OS library draws them instead.
1781 
1782 
1783 		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.
1784 
1785 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
1786 
1787 		History:
1788 			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.
1789 	+/
1790 	void paint(WidgetPainter painter) {
1791 		version(win32_widgets)
1792 			if(hwnd) {
1793 				return;
1794 			}
1795 		painter.drawThemed(&paintContent); // note this refers to the following overload
1796 	}
1797 
1798 	/++
1799 		Responsible for drawing the content as the theme engine is responsible for other elements.
1800 
1801 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
1802 
1803 		Params:
1804 			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.
1805 
1806 			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.
1807 
1808 			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.
1809 
1810 		Returns:
1811 			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.
1812 
1813 		History:
1814 			Added May 15, 2021
1815 	+/
1816 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
1817 		return bounds;
1818 	}
1819 
1820 	deprecated("Change ScreenPainter to WidgetPainter")
1821 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
1822 
1823 	/// I don't actually like the name of this
1824 	/// this draws a background on it
1825 	void erase(WidgetPainter painter) {
1826 		version(win32_widgets)
1827 			if(hwnd) return; // Windows will do it. I think.
1828 
1829 		auto c = getComputedStyle().background.color;
1830 		painter.fillColor = c;
1831 		painter.outlineColor = c;
1832 
1833 		version(win32_widgets) {
1834 			HANDLE b, p;
1835 			if(c.a == 0 && parent is parentWindow) {
1836 				// I don't remember why I had this really...
1837 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
1838 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
1839 			}
1840 		}
1841 		painter.drawRectangle(Point(0, 0), width, height);
1842 		version(win32_widgets) {
1843 			if(c.a == 0 && parent is parentWindow) {
1844 				SelectObject(painter.impl.hdc, p);
1845 				SelectObject(painter.impl.hdc, b);
1846 			}
1847 		}
1848 	}
1849 
1850 	///
1851 	WidgetPainter draw() {
1852 		int x = this.x, y = this.y;
1853 		auto parent = this.parent;
1854 		while(parent) {
1855 			x += parent.x;
1856 			y += parent.y;
1857 			parent = parent.parent;
1858 		}
1859 
1860 		auto painter = parentWindow.win.draw(true);
1861 		painter.originX = x;
1862 		painter.originY = y;
1863 		painter.setClipRectangle(Point(0, 0), width, height);
1864 		return WidgetPainter(painter, this);
1865 	}
1866 
1867 	/// 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.
1868 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
1869 		if(hidden)
1870 			return;
1871 
1872 		int paintX = x;
1873 		int paintY = y;
1874 		if(this.useNativeDrawing()) {
1875 			paintX = 0;
1876 			paintY = 0;
1877 			lox = 0;
1878 			loy = 0;
1879 			containment = Rectangle(0, 0, int.max, int.max);
1880 		}
1881 
1882 		painter.originX = lox + paintX;
1883 		painter.originY = loy + paintY;
1884 
1885 		bool actuallyPainted = false;
1886 
1887 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
1888 		if(clip == Rectangle.init) {
1889 			// writeln(this, " clipped out");
1890 			return;
1891 		}
1892 
1893 		bool invalidateChildren = invalidate;
1894 
1895 		if(redrawRequested || force) {
1896 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
1897 
1898 			painter.drawingUpon = this;
1899 
1900 			erase(painter);
1901 			if(painter.visualTheme)
1902 				painter.visualTheme.doPaint(this, painter);
1903 			else
1904 				paint(painter);
1905 
1906 			if(invalidate) {
1907 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
1908 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
1909 				painter.invalidateRect(region);
1910 				// children are contained inside this, so no need to do extra work
1911 				invalidateChildren = false;
1912 			}
1913 
1914 			redrawRequested = false;
1915 			actuallyPainted = true;
1916 		}
1917 
1918 		foreach(child; children) {
1919 			version(win32_widgets)
1920 				if(child.useNativeDrawing()) continue;
1921 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
1922 		}
1923 
1924 		version(win32_widgets)
1925 		foreach(child; children) {
1926 			if(child.useNativeDrawing) {
1927 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
1928 				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
1929 			}
1930 		}
1931 	}
1932 
1933 	protected bool useNativeDrawing() nothrow {
1934 		version(win32_widgets)
1935 			return hwnd !is null;
1936 		else
1937 			return false;
1938 	}
1939 
1940 	private static class RedrawEvent {}
1941 	private __gshared re = new RedrawEvent();
1942 
1943 	private bool redrawRequested;
1944 	///
1945 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
1946 		redrawRequested = true;
1947 
1948 		if(this.parentWindow) {
1949 			auto sw = this.parentWindow.win;
1950 			assert(sw !is null);
1951 			if(!sw.eventQueued!RedrawEvent) {
1952 				sw.postEvent(re);
1953 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1954 			}
1955 		}
1956 	}
1957 
1958 	private SimpleWindow drawableWindow;
1959 
1960 	/++
1961 		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.
1962 
1963 		Returns:
1964 			`true` if you should do your default behavior.
1965 
1966 		History:
1967 			Added May 5, 2021
1968 
1969 		Bugs:
1970 			It does not do the static checks on gdc right now.
1971 	+/
1972 	final protected bool emit(EventType, this This, Args...)(Args args) {
1973 		version(GNU) {} else
1974 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
1975 		auto e = new EventType(this, args);
1976 		e.dispatch();
1977 		return !e.defaultPrevented;
1978 	}
1979 	/// ditto
1980 	final protected bool emit(string eventString, this This)() {
1981 		auto e = new Event(eventString, this);
1982 		e.dispatch();
1983 		return !e.defaultPrevented;
1984 	}
1985 
1986 	/++
1987 		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.
1988 
1989 		History:
1990 			Added May 5, 2021
1991 	+/
1992 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
1993 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
1994 		return addEventListener(handler);
1995 	}
1996 
1997 	/++
1998 		Gets the computed style properties from the visual theme.
1999 
2000 		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].)
2001 
2002 		History:
2003 			Added May 8, 2021
2004 	+/
2005 	final StyleInformation getComputedStyle() {
2006 		return StyleInformation(this);
2007 	}
2008 
2009 	int focusableWidgets(scope int delegate(Widget) dg) {
2010 		foreach(widget; WidgetStream(this)) {
2011 			if(widget.tabStop && !widget.hidden) {
2012 				int result = dg(widget);
2013 				if (result)
2014 					return result;
2015 			}
2016 		}
2017 		return 0;
2018 	}
2019 
2020 	/++
2021 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2022 		for the given content box (the area between the padding)
2023 
2024 		History:
2025 			Added January 4, 2023 (dub v11.0)
2026 	+/
2027 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2028 		auto cs = getComputedStyle();
2029 
2030 		auto borderWidth = getBorderWidth(cs.borderStyle);
2031 
2032 		auto rect = contentBox;
2033 
2034 		rect.left -= borderWidth;
2035 		rect.right += borderWidth;
2036 		rect.top -= borderWidth;
2037 		rect.bottom += borderWidth;
2038 
2039 		auto insideBorderRect = rect;
2040 
2041 		rect.left -= cs.paddingLeft;
2042 		rect.right += cs.paddingRight;
2043 		rect.top -= cs.paddingTop;
2044 		rect.bottom += cs.paddingBottom;
2045 
2046 		return rect;
2047 	}
2048 
2049 
2050 	// FIXME: I kinda want to hide events from implementation widgets
2051 	// so it just catches them all and stops propagation...
2052 	// i guess i can do it with a event listener on star.
2053 
2054 	mixin Emits!KeyDownEvent; ///
2055 	mixin Emits!KeyUpEvent; ///
2056 	mixin Emits!CharEvent; ///
2057 
2058 	mixin Emits!MouseDownEvent; ///
2059 	mixin Emits!MouseUpEvent; ///
2060 	mixin Emits!ClickEvent; ///
2061 	mixin Emits!DoubleClickEvent; ///
2062 	mixin Emits!MouseMoveEvent; ///
2063 	mixin Emits!MouseOverEvent; ///
2064 	mixin Emits!MouseOutEvent; ///
2065 	mixin Emits!MouseEnterEvent; ///
2066 	mixin Emits!MouseLeaveEvent; ///
2067 
2068 	mixin Emits!ResizeEvent; ///
2069 
2070 	mixin Emits!BlurEvent; ///
2071 	mixin Emits!FocusEvent; ///
2072 
2073 	mixin Emits!FocusInEvent; ///
2074 	mixin Emits!FocusOutEvent; ///
2075 }
2076 
2077 /+
2078 /++
2079 	Interface to indicate that the widget has a simple value property.
2080 
2081 	History:
2082 		Added August 26, 2021
2083 +/
2084 interface HasValue!T {
2085 	/// Getter
2086 	@property T value();
2087 	/// Setter
2088 	@property void value(T);
2089 }
2090 
2091 /++
2092 	Interface to indicate that the widget has a range of possible values for its simple value property.
2093 	This would be present on something like a slider or possibly a number picker.
2094 
2095 	History:
2096 		Added September 11, 2021
2097 +/
2098 interface HasRangeOfValues!T : HasValue!T {
2099 	/// The minimum and maximum values in the range, inclusive.
2100 	@property T minValue();
2101 	@property void minValue(T); /// ditto
2102 	@property T maxValue(); /// ditto
2103 	@property void maxValue(T); /// ditto
2104 
2105 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2106 	@property void step(T);
2107 	@property T step(); /// ditto
2108 }
2109 
2110 /++
2111 	Interface to indicate that the widget has a list of possible values the user can choose from.
2112 	This would be present on something like a drop-down selector.
2113 
2114 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2115 	combobox.
2116 
2117 	History:
2118 		Added September 11, 2021
2119 +/
2120 interface HasListOfValues!T : HasValue!T {
2121 	@property T[] values;
2122 	@property void values(T[]);
2123 
2124 	@property int selectedIndex(); // note it may return -1!
2125 	@property void selectedIndex(int);
2126 }
2127 +/
2128 
2129 /++
2130 	History:
2131 		Added September 2021 (dub v10.4)
2132 +/
2133 class GridLayout : Layout {
2134 
2135 	// 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.
2136 
2137 	/++
2138 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2139 	+/
2140 	enum Gravity {
2141 		Center    = 0,
2142 		NorthWest = North | West,
2143 		North     = 0b10_00,
2144 		NorthEast = North | East,
2145 		West      = 0b00_10,
2146 		East      = 0b00_01,
2147 		SouthWest = South | West,
2148 		South     = 0b01_00,
2149 		SouthEast = South | East,
2150 	}
2151 
2152 	/++
2153 		The width and height are in some proportional units and can often just be 12.
2154 	+/
2155 	this(int width, int height, Widget parent) {
2156 		this.gridWidth = width;
2157 		this.gridHeight = height;
2158 		super(parent);
2159 	}
2160 
2161 	/++
2162 		Sets the position of the given child.
2163 
2164 		The units of these arguments are in the proportional grid units you set in the constructor.
2165 	+/
2166 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2167 		// ensure it is in bounds
2168 		// then ensure no overlaps
2169 
2170 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2171 
2172 		foreach(ref position; positions) {
2173 			if(position.widget is child) {
2174 				position = p;
2175 				goto set;
2176 			}
2177 		}
2178 
2179 		positions ~= p;
2180 
2181 		set:
2182 
2183 		// FIXME: should this batch?
2184 		queueRecomputeChildLayout();
2185 
2186 		return child;
2187 	}
2188 
2189 	override void addChild(Widget w, int position = int.max) {
2190 		super.addChild(w, position);
2191 		//positions ~= ChildPosition(w);
2192 		if(position != int.max) {
2193 			// FIXME: align it so they actually match.
2194 		}
2195 	}
2196 
2197 	override void widgetRemoved(size_t idx, Widget w) {
2198 		// FIXME: keep the positions array aligned
2199 		// positions[idx].widget = null;
2200 	}
2201 
2202 	override void recomputeChildLayout() {
2203 		registerMovement();
2204 		int onGrid = cast(int) positions.length;
2205 		c: foreach(child; children) {
2206 			// just snap it to the grid
2207 			if(onGrid)
2208 			foreach(position; positions)
2209 				if(position.widget is child) {
2210 					child.x = this.width * position.x / this.gridWidth;
2211 					child.y = this.height * position.y / this.gridHeight;
2212 					child.width = this.width * position.width / this.gridWidth;
2213 					child.height = this.height * position.height / this.gridHeight;
2214 
2215 					auto diff = child.width - child.maxWidth();
2216 					// FIXME: gravity?
2217 					if(diff > 0) {
2218 						child.width = child.width - diff;
2219 
2220 						if(position.gravity & Gravity.West) {
2221 							// nothing needed, already aligned
2222 						} else if(position.gravity & Gravity.East) {
2223 							child.x += diff;
2224 						} else {
2225 							child.x += diff / 2;
2226 						}
2227 					}
2228 
2229 					diff = child.height - child.maxHeight();
2230 					// FIXME: gravity?
2231 					if(diff > 0) {
2232 						child.height = child.height - diff;
2233 
2234 						if(position.gravity & Gravity.North) {
2235 							// nothing needed, already aligned
2236 						} else if(position.gravity & Gravity.South) {
2237 							child.y += diff;
2238 						} else {
2239 							child.y += diff / 2;
2240 						}
2241 					}
2242 
2243 
2244 					child.recomputeChildLayout();
2245 					onGrid--;
2246 					continue c;
2247 				}
2248 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2249 		}
2250 	}
2251 
2252 	private struct ChildPosition {
2253 		Widget widget;
2254 		int x;
2255 		int y;
2256 		int width;
2257 		int height;
2258 		Gravity gravity;
2259 	}
2260 	private ChildPosition[] positions;
2261 
2262 	int gridWidth = 12;
2263 	int gridHeight = 12;
2264 }
2265 
2266 ///
2267 abstract class ComboboxBase : Widget {
2268 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2269 	// or to always show the list, we want CBS_SIMPLE == 1
2270 	version(win32_widgets)
2271 		this(uint style, Widget parent) {
2272 			super(parent);
2273 			createWin32Window(this, "ComboBox"w, null, style);
2274 		}
2275 	else version(custom_widgets)
2276 		this(Widget parent) {
2277 			super(parent);
2278 
2279 			addEventListener((KeyDownEvent event) {
2280 				if(event.key == Key.Up) {
2281 					if(selection_ > -1) { // -1 means select blank
2282 						selection_--;
2283 						fireChangeEvent();
2284 					}
2285 					event.preventDefault();
2286 				}
2287 				if(event.key == Key.Down) {
2288 					if(selection_ + 1 < options.length) {
2289 						selection_++;
2290 						fireChangeEvent();
2291 					}
2292 					event.preventDefault();
2293 				}
2294 
2295 			});
2296 
2297 		}
2298 	else static assert(false);
2299 
2300 	/++
2301 		Returns the current list of options in the selection.
2302 
2303 		History:
2304 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2305 	+/
2306 	final @property string[] options() const {
2307 		return cast(string[]) options_;
2308 	}
2309 
2310 	private string[] options_;
2311 	private int selection_ = -1;
2312 
2313 	/++
2314 		Adds an option to the end of options array.
2315 	+/
2316 	void addOption(string s) {
2317 		options_ ~= s;
2318 		version(win32_widgets)
2319 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2320 	}
2321 
2322 	/++
2323 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2324 	+/
2325 	int getSelection() {
2326 		return selection_;
2327 	}
2328 
2329 	/++
2330 		Returns the current selection as a string.
2331 
2332 		History:
2333 			Added November 17, 2021
2334 	+/
2335 	string getSelectionString() {
2336 		return selection_ == -1 ? null : options[selection_];
2337 	}
2338 
2339 	/++
2340 		Sets the current selection to an index in the options array, or to the given option if present.
2341 		Please note that the string version may do a linear lookup.
2342 
2343 		Returns:
2344 			the index you passed in
2345 
2346 		History:
2347 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2348 
2349 			The return value was `void` prior to March 1, 2022.
2350 	+/
2351 	int setSelection(int idx) {
2352 		selection_ = idx;
2353 		version(win32_widgets)
2354 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2355 
2356 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2357 		t.dispatch();
2358 
2359 		return idx;
2360 	}
2361 
2362 	/// ditto
2363 	int setSelection(string s) {
2364 		if(s !is null)
2365 		foreach(idx, item; options)
2366 			if(item == s) {
2367 				return setSelection(cast(int) idx);
2368 			}
2369 		return setSelection(-1);
2370 	}
2371 
2372 	/++
2373 		This event is fired when the selection changes. Note it inherits
2374 		from ChangeEvent!string, meaning you can use that as well, and it also
2375 		fills in [Event.intValue].
2376 	+/
2377 	static class SelectionChangedEvent : ChangeEvent!string {
2378 		this(Widget target, int iv, string sv) {
2379 			super(target, &stringValue);
2380 			this.iv = iv;
2381 			this.sv = sv;
2382 		}
2383 		immutable int iv;
2384 		immutable string sv;
2385 
2386 		override @property string stringValue() { return sv; }
2387 		override @property int intValue() { return iv; }
2388 	}
2389 
2390 	version(win32_widgets)
2391 	override void handleWmCommand(ushort cmd, ushort id) {
2392 		if(cmd == CBN_SELCHANGE) {
2393 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2394 			fireChangeEvent();
2395 		}
2396 	}
2397 
2398 	private void fireChangeEvent() {
2399 		if(selection_ >= options.length)
2400 			selection_ = -1;
2401 
2402 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2403 		t.dispatch();
2404 	}
2405 
2406 	version(win32_widgets) {
2407 		override int minHeight() { return defaultLineHeight + 6; }
2408 		override int maxHeight() { return defaultLineHeight + 6; }
2409 	} else {
2410 		override int minHeight() { return defaultLineHeight + 4; }
2411 		override int maxHeight() { return defaultLineHeight + 4; }
2412 	}
2413 
2414 	version(custom_widgets) {
2415 
2416 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2417 
2418 		SimpleWindow dropDown;
2419 		void popup() {
2420 			auto w = width;
2421 			// FIXME: suggestedDropdownHeight see below
2422 			auto h = cast(int) this.options.length * defaultLineHeight + 8;
2423 
2424 			auto coord = this.globalCoordinates();
2425 			auto dropDown = new SimpleWindow(
2426 				w, h,
2427 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null);
2428 
2429 			dropDown.move(coord.x, coord.y + this.height);
2430 
2431 			{
2432 				auto cs = getComputedStyle();
2433 				auto painter = dropDown.draw();
2434 				draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2435 				auto p = Point(4, 4);
2436 				painter.outlineColor = cs.foregroundColor;
2437 				foreach(option; options) {
2438 					painter.drawText(p, option);
2439 					p.y += defaultLineHeight;
2440 				}
2441 			}
2442 
2443 			dropDown.setEventHandlers(
2444 				(MouseEvent event) {
2445 					if(event.type == MouseEventType.buttonReleased) {
2446 						dropDown.close();
2447 						auto element = (event.y - 4) / defaultLineHeight;
2448 						if(element >= 0 && element <= options.length) {
2449 							selection_ = element;
2450 
2451 							fireChangeEvent();
2452 						}
2453 					}
2454 				}
2455 			);
2456 
2457 			dropDown.visibilityChanged = (bool visible) {
2458 				if(visible) {
2459 					this.redraw();
2460 					dropDown.grabInput();
2461 				} else {
2462 					dropDown.releaseInputGrab();
2463 				}
2464 			};
2465 
2466 			dropDown.show();
2467 		}
2468 
2469 	}
2470 }
2471 
2472 /++
2473 	A drop-down list where the user must select one of the
2474 	given options. Like `<select>` in HTML.
2475 +/
2476 class DropDownSelection : ComboboxBase {
2477 	this(Widget parent) {
2478 		version(win32_widgets)
2479 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
2480 		else version(custom_widgets) {
2481 			super(parent);
2482 
2483 			addEventListener("focus", () { this.redraw; });
2484 			addEventListener("blur", () { this.redraw; });
2485 			addEventListener(EventType.change, () { this.redraw; });
2486 			addEventListener("mousedown", () { this.focus(); this.popup(); });
2487 			addEventListener((KeyDownEvent event) {
2488 				if(event.key == Key.Space)
2489 					popup();
2490 			});
2491 		} else static assert(false);
2492 	}
2493 
2494 	mixin Padding!q{2};
2495 	static class Style : Widget.Style {
2496 		override FrameStyle borderStyle() { return FrameStyle.risen; }
2497 	}
2498 	mixin OverrideStyle!Style;
2499 
2500 	version(custom_widgets)
2501 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2502 		auto cs = getComputedStyle();
2503 
2504 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
2505 
2506 		painter.outlineColor = cs.foregroundColor;
2507 		painter.fillColor = cs.foregroundColor;
2508 
2509 		/+
2510 		Point[4] triangle;
2511 		enum padding = 6;
2512 		enum paddingV = 7;
2513 		enum triangleWidth = 10;
2514 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
2515 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
2516 		triangle[2] = Point(width - padding - 0, paddingV);
2517 		triangle[3] = triangle[0];
2518 		painter.drawPolygon(triangle[]);
2519 		+/
2520 
2521 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
2522 
2523 		painter.drawPolygon(
2524 			scaleWithDpi(Point(2, 6) + offset),
2525 			scaleWithDpi(Point(7, 11) + offset),
2526 			scaleWithDpi(Point(12, 6) + offset),
2527 			scaleWithDpi(Point(2, 6) + offset)
2528 		);
2529 
2530 
2531 		return bounds;
2532 	}
2533 
2534 	version(win32_widgets)
2535 	override void registerMovement() {
2536 		version(win32_widgets) {
2537 			if(hwnd) {
2538 				auto pos = getChildPositionRelativeToParentHwnd(this);
2539 				// the height given to this from Windows' perspective is supposed
2540 				// to include the drop down's height. so I add to it to give some
2541 				// room for that.
2542 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
2543 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
2544 			}
2545 		}
2546 		sendResizeEvent();
2547 	}
2548 }
2549 
2550 /++
2551 	A text box with a drop down arrow listing selections.
2552 	The user can choose from the list, or type their own.
2553 +/
2554 class FreeEntrySelection : ComboboxBase {
2555 	this(Widget parent) {
2556 		version(win32_widgets)
2557 			super(2 /* CBS_DROPDOWN */, parent);
2558 		else version(custom_widgets) {
2559 			super(parent);
2560 			auto hl = new HorizontalLayout(this);
2561 			lineEdit = new LineEdit(hl);
2562 
2563 			tabStop = false;
2564 
2565 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2566 
2567 			auto btn = new class ArrowButton {
2568 				this() {
2569 					super(ArrowDirection.down, hl);
2570 				}
2571 				override int maxHeight() {
2572 					return lineEdit.maxHeight;
2573 				}
2574 			};
2575 			//btn.addDirectEventListener("focus", &lineEdit.focus);
2576 			btn.addEventListener("triggered", &this.popup);
2577 			addEventListener(EventType.change, (Event event) {
2578 				lineEdit.content = event.stringValue;
2579 				lineEdit.focus();
2580 				redraw();
2581 			});
2582 		}
2583 		else static assert(false);
2584 	}
2585 
2586 	version(custom_widgets) {
2587 		LineEdit lineEdit;
2588 	}
2589 }
2590 
2591 /++
2592 	A combination of free entry with a list below it.
2593 +/
2594 class ComboBox : ComboboxBase {
2595 	this(Widget parent) {
2596 		version(win32_widgets)
2597 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
2598 		else version(custom_widgets) {
2599 			super(parent);
2600 			lineEdit = new LineEdit(this);
2601 			listWidget = new ListWidget(this);
2602 			listWidget.multiSelect = false;
2603 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
2604 				string c = null;
2605 				foreach(option; listWidget.options)
2606 					if(option.selected) {
2607 						c = option.label;
2608 						break;
2609 					}
2610 				lineEdit.content = c;
2611 			});
2612 
2613 			listWidget.tabStop = false;
2614 			this.tabStop = false;
2615 			listWidget.addEventListener("focus", &lineEdit.focus);
2616 			this.addEventListener("focus", &lineEdit.focus);
2617 
2618 			addDirectEventListener(EventType.change, {
2619 				listWidget.setSelection(selection_);
2620 				if(selection_ != -1)
2621 					lineEdit.content = options[selection_];
2622 				lineEdit.focus();
2623 				redraw();
2624 			});
2625 
2626 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2627 
2628 			listWidget.addDirectEventListener(EventType.change, {
2629 				int set = -1;
2630 				foreach(idx, opt; listWidget.options)
2631 					if(opt.selected) {
2632 						set = cast(int) idx;
2633 						break;
2634 					}
2635 				if(set != selection_)
2636 					this.setSelection(set);
2637 			});
2638 		} else static assert(false);
2639 	}
2640 
2641 	override int minHeight() { return defaultLineHeight * 3; }
2642 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
2643 	override int heightStretchiness() { return 5; }
2644 
2645 	version(custom_widgets) {
2646 		LineEdit lineEdit;
2647 		ListWidget listWidget;
2648 
2649 		override void addOption(string s) {
2650 			listWidget.options ~= ListWidget.Option(s);
2651 			ComboboxBase.addOption(s);
2652 		}
2653 	}
2654 }
2655 
2656 /+
2657 class Spinner : Widget {
2658 	version(win32_widgets)
2659 	this(Widget parent) {
2660 		super(parent);
2661 		parentWindow = parent.parentWindow;
2662 		auto hlayout = new HorizontalLayout(this);
2663 		lineEdit = new LineEdit(hlayout);
2664 		upDownControl = new UpDownControl(hlayout);
2665 	}
2666 
2667 	LineEdit lineEdit;
2668 	UpDownControl upDownControl;
2669 }
2670 
2671 class UpDownControl : Widget {
2672 	version(win32_widgets)
2673 	this(Widget parent) {
2674 		super(parent);
2675 		parentWindow = parent.parentWindow;
2676 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
2677 	}
2678 
2679 	override int minHeight() { return defaultLineHeight; }
2680 	override int maxHeight() { return defaultLineHeight * 3/2; }
2681 
2682 	override int minWidth() { return defaultLineHeight * 3/2; }
2683 	override int maxWidth() { return defaultLineHeight * 3/2; }
2684 }
2685 +/
2686 
2687 /+
2688 class DataView : Widget {
2689 	// this is the omnibus data viewer
2690 	// the internal data layout is something like:
2691 	// string[string][] but also each node can have parents
2692 }
2693 +/
2694 
2695 
2696 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
2697 
2698 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
2699 
2700 // FIXME: menus should prolly capture the mouse. ugh i kno.
2701 /*
2702 	TextEdit needs:
2703 
2704 	* caret manipulation
2705 	* selection control
2706 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
2707 
2708 	For example:
2709 
2710 	connect(paste, &textEdit.insertTextAtCaret);
2711 
2712 	would be nice.
2713 
2714 
2715 
2716 	I kinda want an omnibus dataview that combines list, tree,
2717 	and table - it can be switched dynamically between them.
2718 
2719 	Flattening policy: only show top level, show recursive, show grouped
2720 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
2721 
2722 	Single select, multi select, organization, drag+drop
2723 */
2724 
2725 //static if(UsingSimpledisplayX11)
2726 version(win32_widgets) {}
2727 else version(custom_widgets) {
2728 	enum scrollClickRepeatInterval = 50;
2729 
2730 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
2731 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
2732 	enum activeTabColor = lightAccentColor;
2733 	enum hoveringColor = Color(228, 228, 228);
2734 	enum buttonColor = windowBackgroundColor;
2735 	enum depressedButtonColor = darkAccentColor;
2736 	enum activeListXorColor = Color(255, 255, 127);
2737 	enum progressBarColor = Color(0, 0, 128);
2738 	enum activeMenuItemColor = Color(0, 0, 128);
2739 
2740 }}
2741 else static assert(false);
2742 deprecated("Get these properties off the `visualTheme` instead.") {
2743 	// these are used by horizontal rule so not just custom_widgets. for now at least.
2744 	enum darkAccentColor = Color(172, 172, 172);
2745 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
2746 }
2747 
2748 private const(wchar)* toWstringzInternal(in char[] s) {
2749 	wchar[] str;
2750 	str.reserve(s.length + 1);
2751 	foreach(dchar ch; s)
2752 		str ~= ch;
2753 	str ~= '\0';
2754 	return str.ptr;
2755 }
2756 
2757 static if(SimpledisplayTimerAvailable)
2758 void setClickRepeat(Widget w, int interval, int delay = 250) {
2759 	Timer timer;
2760 	int delayRemaining = delay / interval;
2761 	if(delayRemaining <= 1)
2762 		delayRemaining = 2;
2763 
2764 	immutable originalDelayRemaining = delayRemaining;
2765 
2766 	w.addDirectEventListener((scope MouseDownEvent ev) {
2767 		if(ev.srcElement !is w)
2768 			return;
2769 		if(timer !is null) {
2770 			timer.destroy();
2771 			timer = null;
2772 		}
2773 		delayRemaining = originalDelayRemaining;
2774 		timer = new Timer(interval, () {
2775 			if(delayRemaining > 0)
2776 				delayRemaining--;
2777 			else {
2778 				auto ev = new Event("triggered", w);
2779 				ev.sendDirectly();
2780 			}
2781 		});
2782 	});
2783 
2784 	w.addDirectEventListener((scope MouseUpEvent ev) {
2785 		if(ev.srcElement !is w)
2786 			return;
2787 		if(timer !is null) {
2788 			timer.destroy();
2789 			timer = null;
2790 		}
2791 	});
2792 
2793 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
2794 		if(ev.srcElement !is w)
2795 			return;
2796 		if(timer !is null) {
2797 			timer.destroy();
2798 			timer = null;
2799 		}
2800 	});
2801 
2802 }
2803 else
2804 void setClickRepeat(Widget w, int interval, int delay = 250) {}
2805 
2806 enum FrameStyle {
2807 	none, ///
2808 	risen, /// a 3d pop-out effect (think Windows 95 button)
2809 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
2810 	solid, ///
2811 	dotted, ///
2812 	fantasy, /// a style based on a popular fantasy video game
2813 }
2814 
2815 version(custom_widgets)
2816 deprecated
2817 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
2818 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2819 }
2820 
2821 version(custom_widgets)
2822 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
2823 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
2824 }
2825 
2826 version(custom_widgets)
2827 deprecated
2828 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
2829 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2830 }
2831 
2832 int getBorderWidth(FrameStyle style) {
2833 	final switch(style) {
2834 		case FrameStyle.sunk, FrameStyle.risen:
2835 			return 2;
2836 		case FrameStyle.none:
2837 			return 0;
2838 		case FrameStyle.solid:
2839 			return 1;
2840 		case FrameStyle.dotted:
2841 			return 1;
2842 		case FrameStyle.fantasy:
2843 			return 3;
2844 	}
2845 }
2846 
2847 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
2848 	int borderWidth = getBorderWidth(style);
2849 	final switch(style) {
2850 		case FrameStyle.sunk, FrameStyle.risen:
2851 			// outer layer
2852 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
2853 		break;
2854 		case FrameStyle.none:
2855 			painter.outlineColor = background;
2856 		break;
2857 		case FrameStyle.solid:
2858 			painter.pen = Pen(border, 1);
2859 		break;
2860 		case FrameStyle.dotted:
2861 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
2862 		break;
2863 		case FrameStyle.fantasy:
2864 			painter.pen = Pen(border, 3);
2865 		break;
2866 	}
2867 
2868 	painter.fillColor = background;
2869 	painter.drawRectangle(Point(x + 0, y + 0), width, height);
2870 
2871 
2872 	if(style == FrameStyle.sunk || style == FrameStyle.risen) {
2873 		// 3d effect
2874 		auto vt = WidgetPainter.visualTheme;
2875 
2876 		painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
2877 		painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
2878 		painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
2879 
2880 		// inner layer
2881 		//right, bottom
2882 		painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
2883 		painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
2884 		painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
2885 		// left, top
2886 		painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
2887 		painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
2888 		painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
2889 	} else if(style == FrameStyle.fantasy) {
2890 		painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
2891 		painter.fillColor = Color.transparent;
2892 		painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
2893 	}
2894 
2895 	return borderWidth;
2896 }
2897 
2898 /++
2899 	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.
2900 
2901 	See_Also:
2902 		[MenuItem]
2903 		[ToolButton]
2904 		[Menu.addItem]
2905 +/
2906 class Action {
2907 	version(win32_widgets) {
2908 		private int id;
2909 		private static int lastId = 9000;
2910 		private static Action[int] mapping;
2911 	}
2912 
2913 	KeyEvent accelerator;
2914 
2915 	// FIXME: disable message
2916 	// and toggle thing?
2917 	// ??? and trigger arguments too ???
2918 
2919 	/++
2920 		Params:
2921 			label = the textual label
2922 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
2923 			triggered = initial handler, more can be added via the [triggered] member.
2924 	+/
2925 	this(string label, ushort icon = 0, void delegate() triggered = null) {
2926 		this.label = label;
2927 		this.iconId = icon;
2928 		if(triggered !is null)
2929 			this.triggered ~= triggered;
2930 		version(win32_widgets) {
2931 			id = ++lastId;
2932 			mapping[id] = this;
2933 		}
2934 	}
2935 
2936 	private string label;
2937 	private ushort iconId;
2938 	// icon
2939 
2940 	// when it is triggered, the triggered event is fired on the window
2941 	/// The list of handlers when it is triggered.
2942 	void delegate()[] triggered;
2943 }
2944 
2945 /*
2946 	plan:
2947 		keyboard accelerators
2948 
2949 		* menus (and popups and tooltips)
2950 		* status bar
2951 		* toolbars and buttons
2952 
2953 		sortable table view
2954 
2955 		maybe notification area icons
2956 		basic clipboard
2957 
2958 		* radio box
2959 		splitter
2960 		toggle buttons (optionally mutually exclusive, like in Paint)
2961 		label, rich text display, multi line plain text (selectable)
2962 		* fieldset
2963 		* nestable grid layout
2964 		single line text input
2965 		* multi line text input
2966 		slider
2967 		spinner
2968 		list box
2969 		drop down
2970 		combo box
2971 		auto complete box
2972 		* progress bar
2973 
2974 		terminal window/widget (on unix it might even be a pty but really idk)
2975 
2976 		ok button
2977 		cancel button
2978 
2979 		keyboard hotkeys
2980 
2981 		scroll widget
2982 
2983 		event redirections and network transparency
2984 		script integration
2985 */
2986 
2987 
2988 /*
2989 	MENUS
2990 
2991 	auto bar = new MenuBar(window);
2992 	window.menuBar = bar;
2993 
2994 	auto fileMenu = bar.addItem(new Menu("&File"));
2995 	fileMenu.addItem(new MenuItem("&Exit"));
2996 
2997 
2998 	EVENTS
2999 
3000 	For controls, you should usually use "triggered" rather than "click", etc., because
3001 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3002 	This is the case on menus and pushbuttons.
3003 
3004 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3005 */
3006 
3007 
3008 /*
3009 enum LinePreference {
3010 	AlwaysOnOwnLine, // always on its own line
3011 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3012 	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
3013 }
3014 */
3015 
3016 /++
3017 	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.
3018 
3019 	---
3020 	class MyWidget : Widget {
3021 		this(Widget parent) { super(parent); }
3022 
3023 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3024 		mixin Padding!q{4};
3025 
3026 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3027 		mixin Margin!q{8};
3028 
3029 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3030 		// while Top/Bottom/Right remain 8 from the mixin above.
3031 		override int marginLeft() { return 2; }
3032 	}
3033 	---
3034 
3035 
3036 	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]).
3037 
3038 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3039 
3040 	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!).
3041 
3042 	* 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.
3043 +/
3044 mixin template Padding(string code) {
3045 	override int paddingLeft() { return mixin(code);}
3046 	override int paddingRight() { return mixin(code);}
3047 	override int paddingTop() { return mixin(code);}
3048 	override int paddingBottom() { return mixin(code);}
3049 }
3050 
3051 /// ditto
3052 mixin template Margin(string code) {
3053 	override int marginLeft() { return mixin(code);}
3054 	override int marginRight() { return mixin(code);}
3055 	override int marginTop() { return mixin(code);}
3056 	override int marginBottom() { return mixin(code);}
3057 }
3058 
3059 private
3060 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3061 	enum calcingV = relevantMeasure == "height";
3062 
3063 	parent.registerMovement();
3064 
3065 	if(parent.children.length == 0)
3066 		return;
3067 
3068 	auto parentStyle = parent.getComputedStyle();
3069 
3070 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3071 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3072 
3073 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3074 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3075 
3076 	// my own width and height should already be set by the caller of this function...
3077 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3078 		mixin("parentStyle.padding"~firstThingy~"()") -
3079 		mixin("parentStyle.padding"~secondThingy~"()");
3080 
3081 	int stretchinessSum;
3082 	int stretchyChildSum;
3083 	int lastMargin = 0;
3084 
3085 	int shrinkinessSum;
3086 	int shrinkyChildSum;
3087 
3088 	// set initial size
3089 	foreach(child; parent.children) {
3090 
3091 		auto childStyle = child.getComputedStyle();
3092 
3093 		if(cast(StaticPosition) child)
3094 			continue;
3095 		if(child.hidden)
3096 			continue;
3097 
3098 		const iw = child.flexBasisWidth();
3099 		const ih = child.flexBasisHeight();
3100 
3101 		static if(calcingV) {
3102 			child.width = parent.width -
3103 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3104 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3105 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3106 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3107 
3108 			if(child.width < 0)
3109 				child.width = 0;
3110 			if(child.width > childStyle.maxWidth())
3111 				child.width = childStyle.maxWidth();
3112 
3113 			if(iw > 0) {
3114 				auto totalPossible = child.width;
3115 				if(child.width > iw && child.widthStretchiness() == 0)
3116 					child.width = iw;
3117 			}
3118 
3119 			child.height = mymax(childStyle.minHeight(), ih);
3120 		} else {
3121 			// set to take all the space
3122 			child.height = parent.height -
3123 				mixin("childStyle.margin"~firstThingy~"()") -
3124 				mixin("childStyle.margin"~secondThingy~"()") -
3125 				mixin("parentStyle.padding"~firstThingy~"()") -
3126 				mixin("parentStyle.padding"~secondThingy~"()");
3127 
3128 			// then clamp it
3129 			if(child.height < 0)
3130 				child.height = 0;
3131 			if(child.height > childStyle.maxHeight())
3132 				child.height = childStyle.maxHeight();
3133 
3134 			// and if possible, respect the ideal target
3135 			if(ih > 0) {
3136 				auto totalPossible = child.height;
3137 				if(child.height > ih && child.heightStretchiness() == 0)
3138 					child.height = ih;
3139 			}
3140 
3141 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3142 			child.width = mymax(childStyle.minWidth(), iw);
3143 		}
3144 
3145 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3146 
3147 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3148 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3149 		lastMargin = margin;
3150 		spaceRemaining -= thisMargin + margin;
3151 
3152 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3153 		stretchinessSum += s;
3154 		if(s > 0)
3155 			stretchyChildSum++;
3156 
3157 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3158 		shrinkinessSum += s2;
3159 		if(s2 > 0)
3160 			shrinkyChildSum++;
3161 	}
3162 
3163 	if(spaceRemaining < 0 && shrinkyChildSum) {
3164 		// shrink to get into the space if it is possible
3165 		auto toRemove = -spaceRemaining;
3166 		auto removalPerItem  = toRemove * shrinkinessSum / shrinkyChildSum;
3167 		auto remainder = toRemove * shrinkinessSum % shrinkyChildSum;
3168 
3169 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3170 
3171 		foreach(child; parent.children) {
3172 			auto childStyle = child.getComputedStyle();
3173 			if(cast(StaticPosition) child)
3174 				continue;
3175 			if(child.hidden)
3176 				continue;
3177 			static if(calcingV) {
3178 				auto maximum = childStyle.maxHeight();
3179 			} else {
3180 				auto maximum = childStyle.maxWidth();
3181 			}
3182 
3183 			if(mixin("child._" ~ relevantMeasure) >= maximum)
3184 				continue;
3185 
3186 			mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh.
3187 
3188 			spaceRemaining += removalPerItem + remainder;
3189 		}
3190 	}
3191 
3192 	// stretch to fill space
3193 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3194 		auto spacePerChild = spaceRemaining / stretchinessSum;
3195 		bool spreadEvenly;
3196 		bool giveToBiggest;
3197 		if(spacePerChild <= 0) {
3198 			spacePerChild = spaceRemaining / stretchyChildSum;
3199 			spreadEvenly = true;
3200 		}
3201 		if(spacePerChild <= 0) {
3202 			giveToBiggest = true;
3203 		}
3204 		int previousSpaceRemaining = spaceRemaining;
3205 		stretchinessSum = 0;
3206 		Widget mostStretchy;
3207 		int mostStretchyS;
3208 		foreach(child; parent.children) {
3209 			auto childStyle = child.getComputedStyle();
3210 			if(cast(StaticPosition) child)
3211 				continue;
3212 			if(child.hidden)
3213 				continue;
3214 			static if(calcingV) {
3215 				auto maximum = childStyle.maxHeight();
3216 			} else {
3217 				auto maximum = childStyle.maxWidth();
3218 			}
3219 
3220 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3221 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3222 				mixin("child._" ~ relevantMeasure) -= adj;
3223 				spaceRemaining += adj;
3224 				continue;
3225 			}
3226 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3227 			if(s <= 0)
3228 				continue;
3229 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3230 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3231 			spaceRemaining -= spaceAdjustment;
3232 			if(mixin("child." ~ relevantMeasure) > maximum) {
3233 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3234 				mixin("child._" ~ relevantMeasure) -= diff;
3235 				spaceRemaining += diff;
3236 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3237 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3238 				if(mostStretchy is null || s >= mostStretchyS) {
3239 					mostStretchy = child;
3240 					mostStretchyS = s;
3241 				}
3242 			}
3243 		}
3244 
3245 		if(giveToBiggest && mostStretchy !is null) {
3246 			auto child = mostStretchy;
3247 			auto childStyle = child.getComputedStyle();
3248 			int spaceAdjustment = spaceRemaining;
3249 
3250 			static if(calcingV)
3251 				auto maximum = childStyle.maxHeight();
3252 			else
3253 				auto maximum = childStyle.maxWidth();
3254 
3255 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3256 			spaceRemaining -= spaceAdjustment;
3257 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3258 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3259 				mixin("child._" ~ relevantMeasure) -= diff;
3260 				spaceRemaining += diff;
3261 			}
3262 		}
3263 
3264 		if(spaceRemaining == previousSpaceRemaining) {
3265 			if(mostStretchy !is null) {
3266 				static if(calcingV)
3267 					auto maximum = mostStretchy.maxHeight();
3268 				else
3269 					auto maximum = mostStretchy.maxWidth();
3270 
3271 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3272 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3273 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3274 			}
3275 			break; // apparently nothing more we can do
3276 		}
3277 	}
3278 
3279 	foreach(child; parent.children) {
3280 		auto childStyle = child.getComputedStyle();
3281 		if(cast(StaticPosition) child)
3282 			continue;
3283 		if(child.hidden)
3284 			continue;
3285 
3286 		static if(calcingV)
3287 			auto maximum = childStyle.maxHeight();
3288 		else
3289 			auto maximum = childStyle.maxWidth();
3290 		if(mixin("child._" ~ relevantMeasure) > maximum)
3291 			mixin("child._" ~ relevantMeasure) = maximum;
3292 	}
3293 
3294 	// position
3295 	lastMargin = 0;
3296 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3297 	foreach(child; parent.children) {
3298 		auto childStyle = child.getComputedStyle();
3299 		if(cast(StaticPosition) child) {
3300 			child.recomputeChildLayout();
3301 			continue;
3302 		}
3303 		if(child.hidden)
3304 			continue;
3305 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3306 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3307 		currentPos += thisMargin;
3308 		static if(calcingV) {
3309 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3310 			child.y = currentPos;
3311 		} else {
3312 			child.x = currentPos;
3313 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3314 
3315 		}
3316 		currentPos += mixin("child." ~ relevantMeasure);
3317 		currentPos += margin;
3318 		lastMargin = margin;
3319 
3320 		child.recomputeChildLayout();
3321 	}
3322 }
3323 
3324 int mymax(int a, int b) { return a > b ? a : b; }
3325 int mymax(int a, int b, int c) {
3326 	auto d = mymax(a, b);
3327 	return c > d ? c : d;
3328 }
3329 
3330 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3331 // and here, it must be integrable with the layout, the event system, and not be painted over.
3332 version(win32_widgets) {
3333 
3334 	// this function just does stuff that a parent window needs for redirection
3335 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3336 		this_.hookedWndProc(msg, wParam, lParam);
3337 
3338 		switch(msg) {
3339 
3340 			case WM_VSCROLL, WM_HSCROLL:
3341 				auto pos = HIWORD(wParam);
3342 				auto m = LOWORD(wParam);
3343 
3344 				auto scrollbarHwnd = cast(HWND) lParam;
3345 
3346 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3347 
3348 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3349 
3350 					switch(m) {
3351 						/+
3352 						// I don't think those messages are ever actually sent normally by the widget itself,
3353 						// they are more used for the keyboard interface. methinks.
3354 						case SB_BOTTOM:
3355 							// writeln("end");
3356 							auto event = new Event("scrolltoend", *widgetp);
3357 							event.dispatch();
3358 							//if(!event.defaultPrevented)
3359 						break;
3360 						case SB_TOP:
3361 							// writeln("top");
3362 							auto event = new Event("scrolltobeginning", *widgetp);
3363 							event.dispatch();
3364 						break;
3365 						case SB_ENDSCROLL:
3366 							// idk
3367 						break;
3368 						+/
3369 						case SB_LINEDOWN:
3370 							(*widgetp).emitCommand!"scrolltonextline"();
3371 						return 0;
3372 						case SB_LINEUP:
3373 							(*widgetp).emitCommand!"scrolltopreviousline"();
3374 						return 0;
3375 						case SB_PAGEDOWN:
3376 							(*widgetp).emitCommand!"scrolltonextpage"();
3377 						return 0;
3378 						case SB_PAGEUP:
3379 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3380 						return 0;
3381 						case SB_THUMBPOSITION:
3382 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3383 							ev.dispatch();
3384 						return 0;
3385 						case SB_THUMBTRACK:
3386 							// eh kinda lying but i like the real time update display
3387 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3388 							ev.dispatch();
3389 
3390 							// the event loop doesn't seem to carry on with a requested redraw..
3391 							// so we request it to get our dirty bit set...
3392 							// then we need to immediately actually redraw it too for instant feedback to user
3393 							SimpleWindow.processAllCustomEvents();
3394 							SimpleWindow.processAllCustomEvents();
3395 							//if(this_.parentWindow)
3396 								//this_.parentWindow.actualRedraw();
3397 
3398 							// and this ensures the WM_PAINT message is sent fairly quickly
3399 							// still seems to lag a little in large windows but meh it basically works.
3400 							if(this_.parentWindow) {
3401 								// FIXME: if painting is slow, this does still lag
3402 								// we probably will want to expose some user hook to ScrollWindowEx
3403 								// or something.
3404 								UpdateWindow(this_.parentWindow.hwnd);
3405 							}
3406 						return 0;
3407 						default:
3408 					}
3409 				}
3410 			break;
3411 
3412 			case WM_CONTEXTMENU:
3413 				auto hwndFrom = cast(HWND) wParam;
3414 
3415 				auto xPos = cast(short) LOWORD(lParam);
3416 				auto yPos = cast(short) HIWORD(lParam);
3417 
3418 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3419 					POINT p;
3420 					p.x = xPos;
3421 					p.y = yPos;
3422 					ScreenToClient(hwnd, &p);
3423 					auto clientX = cast(ushort) p.x;
3424 					auto clientY = cast(ushort) p.y;
3425 
3426 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
3427 
3428 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
3429 						return 0;
3430 					}
3431 				}
3432 			break;
3433 
3434 			case WM_DRAWITEM:
3435 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
3436 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
3437 					return (*widgetp).handleWmDrawItem(dis);
3438 				}
3439 			break;
3440 
3441 			case WM_NOTIFY:
3442 				auto hdr = cast(NMHDR*) lParam;
3443 				auto hwndFrom = hdr.hwndFrom;
3444 				auto code = hdr.code;
3445 
3446 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3447 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
3448 				}
3449 			break;
3450 			case WM_COMMAND:
3451 				auto handle = cast(HWND) lParam;
3452 				auto cmd = HIWORD(wParam);
3453 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
3454 
3455 			default:
3456 				// pass it on
3457 		}
3458 		return 0;
3459 	}
3460 
3461 
3462 
3463 	extern(Windows)
3464 	private
3465 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
3466 	// but can i merge them?!
3467 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3468 		// try { writeln(iMessage); } catch(Exception e) {};
3469 
3470 		if(auto te = hWnd in Widget.nativeMapping) {
3471 			try {
3472 
3473 				te.hookedWndProc(iMessage, wParam, lParam);
3474 
3475 				int mustReturn;
3476 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
3477 				if(mustReturn)
3478 					return ret;
3479 
3480 				if(iMessage == WM_SETFOCUS) {
3481 					auto lol = *te;
3482 					while(lol !is null && lol.implicitlyCreated)
3483 						lol = lol.parent;
3484 					lol.focus();
3485 					//(*te).parentWindow.focusedWidget = lol;
3486 				}
3487 
3488 
3489 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
3490 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
3491 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
3492 						//GetStockObject(NULL_BRUSH);
3493 				}
3494 
3495 				auto pos = getChildPositionRelativeToParentOrigin(*te);
3496 				lastDefaultPrevented = false;
3497 				// try { writeln(typeid(*te)); } catch(Exception e) {}
3498 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
3499 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
3500 				else {
3501 					// it was something we recognized, should only call the window procedure if the default was not prevented
3502 				}
3503 			} catch(Exception e) {
3504 				assert(0, e.toString());
3505 			}
3506 			return 0;
3507 		}
3508 		assert(0, "shouldn't be receiving messages for this window....");
3509 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
3510 	}
3511 
3512 	extern(Windows)
3513 	private
3514 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
3515 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3516 		if(iMessage == WM_ERASEBKGND) {
3517 			auto dc = GetDC(hWnd);
3518 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
3519 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
3520 			RECT r;
3521 			GetWindowRect(hWnd, &r);
3522 			// since the pen is null, to fill the whole space, we need the +1 on both.
3523 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
3524 			SelectObject(dc, p);
3525 			SelectObject(dc, b);
3526 			ReleaseDC(hWnd, dc);
3527 			InvalidateRect(hWnd, null, false); // redraw the border
3528 			return 1;
3529 		}
3530 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
3531 	}
3532 
3533 	/++
3534 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
3535 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
3536 		of minigui's expectations.
3537 
3538 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
3539 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
3540 
3541 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
3542 
3543 		To check if you can use this, use `static if(UsingWin32Widgets)`.
3544 	+/
3545 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
3546 		assert(p.parentWindow !is null);
3547 		assert(p.parentWindow.win.impl.hwnd !is null);
3548 
3549 		auto bsgroupbox = style == BS_GROUPBOX;
3550 
3551 		HWND phwnd;
3552 
3553 		auto wtf = p.parent;
3554 		while(wtf) {
3555 			if(wtf.hwnd !is null) {
3556 				phwnd = wtf.hwnd;
3557 				break;
3558 			}
3559 			wtf = wtf.parent;
3560 		}
3561 
3562 		if(phwnd is null)
3563 			phwnd = p.parentWindow.win.impl.hwnd;
3564 
3565 		assert(phwnd !is null);
3566 
3567 		WCharzBuffer wt = WCharzBuffer(windowText);
3568 
3569 		style |= WS_VISIBLE | WS_CHILD;
3570 		//if(className != WC_TABCONTROL)
3571 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
3572 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
3573 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
3574 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
3575 
3576 		assert(p.hwnd !is null);
3577 
3578 
3579 		static HFONT font;
3580 		if(font is null) {
3581 			NONCLIENTMETRICS params;
3582 			params.cbSize = params.sizeof;
3583 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
3584 				font = CreateFontIndirect(&params.lfMessageFont);
3585 			}
3586 		}
3587 
3588 		if(font)
3589 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
3590 
3591 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
3592 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
3593 		Widget.nativeMapping[p.hwnd] = p;
3594 
3595 		if(bsgroupbox)
3596 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
3597 		else
3598 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3599 
3600 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
3601 
3602 		p.registerMovement();
3603 	}
3604 }
3605 
3606 version(win32_widgets)
3607 private
3608 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
3609 	if(hwnd is null || hwnd in Widget.nativeMapping)
3610 		return true;
3611 	auto parent = cast(Widget) cast(void*) lparam;
3612 	Widget p = new Widget(null);
3613 	p._parent = parent;
3614 	p.parentWindow = parent.parentWindow;
3615 	p.hwnd = hwnd;
3616 	p.implicitlyCreated = true;
3617 	Widget.nativeMapping[p.hwnd] = p;
3618 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3619 	return true;
3620 }
3621 
3622 /++
3623 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
3624 +/
3625 struct WidgetPainter {
3626 	this(ScreenPainter screenPainter, Widget drawingUpon) {
3627 		this.drawingUpon = drawingUpon;
3628 		this.screenPainter = screenPainter;
3629 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3630 			this.screenPainter.setFont(font);
3631 	}
3632 
3633 	/++
3634 		EXPERIMENTAL. subject to change.
3635 
3636 		When you draw a cursor, you can draw this to notify your window of where it is,
3637 		for IME systems to use.
3638 	+/
3639 	void notifyCursorPosition(int x, int y, int width, int height) {
3640 		if(auto a = drawingUpon.parentWindow)
3641 		if(auto w = a.inputProxy) {
3642 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
3643 		}
3644 	}
3645 
3646 
3647 	///
3648 	ScreenPainter screenPainter;
3649 	/// Forward to the screen painter for other methods
3650 	alias screenPainter this;
3651 
3652 	private Widget drawingUpon;
3653 
3654 	/++
3655 		This is the list of rectangles that actually need to be redrawn.
3656 
3657 		Not actually implemented yet.
3658 	+/
3659 	Rectangle[] invalidatedRectangles;
3660 
3661 	private static BaseVisualTheme _visualTheme;
3662 
3663 	/++
3664 		Functions to access the visual theme and helpers to easily use it.
3665 
3666 		These are aware of the current widget's computed style out of the theme.
3667 	+/
3668 	static @property BaseVisualTheme visualTheme() {
3669 		if(_visualTheme is null)
3670 			_visualTheme = new DefaultVisualTheme();
3671 		return _visualTheme;
3672 	}
3673 
3674 	/// ditto
3675 	static @property void visualTheme(BaseVisualTheme theme) {
3676 		_visualTheme = theme;
3677 
3678 		// FIXME: notify all windows about the new theme
3679 	}
3680 
3681 	/// ditto
3682 	Color themeForeground() {
3683 		return drawingUpon.getComputedStyle().foregroundColor();
3684 	}
3685 
3686 	/// ditto
3687 	Color themeBackground() {
3688 		return drawingUpon.getComputedStyle().background.color;
3689 	}
3690 
3691 	int isDarkTheme() {
3692 		return 0; // unspecified, yes, no as enum. FIXME
3693 	}
3694 
3695 	/++
3696 		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.
3697 
3698 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
3699 
3700 		If you change teh clip rectangle, you should change it back before you return.
3701 
3702 
3703 		The sequence it uses is:
3704 			background
3705 			content (delegated to you)
3706 			border
3707 			focused outline
3708 			selected overlay
3709 
3710 		Example code:
3711 
3712 		---
3713 		void paint(WidgetPainter painter) {
3714 			painter.drawThemed((bounds) {
3715 				return bounds; // if the selection overlay should be contained, you can return it here.
3716 			});
3717 		}
3718 		---
3719 	+/
3720 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
3721 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
3722 			return drawBody(bounds);
3723 		});
3724 	}
3725 	// this overload is actually mroe for setting the delegate to a virtual function
3726 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
3727 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
3728 
3729 		auto cs = drawingUpon.getComputedStyle();
3730 
3731 		auto bg = cs.background.color;
3732 
3733 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
3734 
3735 		rect.left += borderWidth;
3736 		rect.right -= borderWidth;
3737 		rect.top += borderWidth;
3738 		rect.bottom -= borderWidth;
3739 
3740 		auto insideBorderRect = rect;
3741 
3742 		rect.left += cs.paddingLeft;
3743 		rect.right -= cs.paddingRight;
3744 		rect.top += cs.paddingTop;
3745 		rect.bottom -= cs.paddingBottom;
3746 
3747 		this.outlineColor = this.themeForeground;
3748 		this.fillColor = bg;
3749 
3750 		auto widgetFont = cs.fontCached;
3751 		if(widgetFont !is null)
3752 			this.setFont(widgetFont);
3753 
3754 		rect = drawBody(this, rect);
3755 
3756 		if(widgetFont !is null) {
3757 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3758 				this.setFont(vtFont);
3759 			else
3760 				this.setFont(null);
3761 		}
3762 
3763 		if(auto os = cs.outlineStyle()) {
3764 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
3765 			this.fillColor = Color.transparent;
3766 			this.drawRectangle(insideBorderRect);
3767 		}
3768 	}
3769 
3770 	/++
3771 		First, draw the background.
3772 		Then draw your content.
3773 		Next, draw the border.
3774 		And the focused indicator.
3775 		And the is-selected box.
3776 
3777 		If it is focused i can draw the outline too...
3778 
3779 		If selected i can even do the xor action but that's at the end.
3780 	+/
3781 	void drawThemeBackground() {
3782 
3783 	}
3784 
3785 	void drawThemeBorder() {
3786 
3787 	}
3788 
3789 	// all this stuff is a dangerous experiment....
3790 	static class ScriptableVersion {
3791 		ScreenPainterImplementation* p;
3792 		int originX, originY;
3793 
3794 		@scriptable:
3795 		void drawRectangle(int x, int y, int width, int height) {
3796 			p.drawRectangle(x + originX, y + originY, width, height);
3797 		}
3798 		void drawLine(int x1, int y1, int x2, int y2) {
3799 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
3800 		}
3801 		void drawText(int x, int y, string text) {
3802 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
3803 		}
3804 		void setOutlineColor(int r, int g, int b) {
3805 			p.pen = Pen(Color(r,g,b), 1);
3806 		}
3807 		void setFillColor(int r, int g, int b) {
3808 			p.fillColor = Color(r,g,b);
3809 		}
3810 	}
3811 
3812 	ScriptableVersion toArsdJsvar() {
3813 		auto sv = new ScriptableVersion;
3814 		sv.p = this.screenPainter.impl;
3815 		sv.originX = this.screenPainter.originX;
3816 		sv.originY = this.screenPainter.originY;
3817 		return sv;
3818 	}
3819 
3820 	static WidgetPainter fromJsVar(T)(T t) {
3821 		return WidgetPainter.init;
3822 	}
3823 	// done..........
3824 }
3825 
3826 
3827 struct Style {
3828 	static struct helper(string m, T) {
3829 		enum method = m;
3830 		T v;
3831 
3832 		mixin template MethodOverride(typeof(this) v) {
3833 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
3834 		}
3835 	}
3836 
3837 	static auto opDispatch(string method, T)(T value) {
3838 		return helper!(method, T)(value);
3839 	}
3840 }
3841 
3842 /++
3843 	Implementation detail of the [ControlledBy] UDA.
3844 
3845 	History:
3846 		Added Oct 28, 2020
3847 +/
3848 struct ControlledBy_(T, Args...) {
3849 	Args args;
3850 
3851 	static if(Args.length)
3852 	this(Args args) {
3853 		this.args = args;
3854 	}
3855 
3856 	private T construct(Widget parent) {
3857 		return new T(args, parent);
3858 	}
3859 }
3860 
3861 /++
3862 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
3863 
3864 	History:
3865 		Added Oct 28, 2020
3866 +/
3867 auto ControlledBy(T, Args...)(Args args) {
3868 	return ControlledBy_!(T, Args)(args);
3869 }
3870 
3871 struct ContainerMeta {
3872 	string name;
3873 	ContainerMeta[] children;
3874 	Widget function(Widget parent) factory;
3875 
3876 	Widget instantiate(Widget parent) {
3877 		auto n = factory(parent);
3878 		n.name = name;
3879 		foreach(child; children)
3880 			child.instantiate(n);
3881 		return n;
3882 	}
3883 }
3884 
3885 /++
3886 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
3887 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
3888 
3889 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
3890 	structures. It works fine on structs declared inside functions though.
3891 
3892 	See: https://issues.dlang.org/show_bug.cgi?id=21984
3893 +/
3894 template Container(CArgs...) {
3895 	static if(CArgs.length && is(CArgs[0] : Widget)) {
3896 		private alias Super = CArgs[0];
3897 		private alias CArgs2 = CArgs[1 .. $];
3898 	} else {
3899 		private alias Super = Layout;
3900 		private alias CArgs2 = CArgs;
3901 	}
3902 
3903 	class Container : Super {
3904 		this(Widget parent) { super(parent); }
3905 
3906 		// just to partially support old gdc versions
3907 		version(GNU) {
3908 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
3909 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
3910 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
3911 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
3912 		} else mixin(q{
3913 			static foreach(Arg; CArgs2) {
3914 				mixin Arg.MethodOverride!(Arg);
3915 			}
3916 		});
3917 
3918 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
3919 			return ContainerMeta(
3920 				name,
3921 				children.dup,
3922 				function (Widget parent) { return new typeof(this)(parent); }
3923 			);
3924 		}
3925 
3926 		static ContainerMeta opCall(ContainerMeta[] children...) {
3927 			return opCall(null, children);
3928 		}
3929 	}
3930 }
3931 
3932 /++
3933 	The data controller widget is created by reflecting over the given
3934 	data type. You can use [ControlledBy] as a UDA on a struct or
3935 	just let it create things automatically.
3936 
3937 	Unlike [dialog], this uses real-time updating of the data and
3938 	you add it to another window yourself.
3939 
3940 	---
3941 		struct Test {
3942 			int x;
3943 			int y;
3944 		}
3945 
3946 		auto window = new Window();
3947 		auto dcw = new DataControllerWidget!Test(new Test, window);
3948 	---
3949 
3950 	The way it works is any public members are given a widget based
3951 	on their data type, and public methods trigger an action button
3952 	if no relevant parameters or a dialog action if it does have
3953 	parameters, similar to the [menu] facility.
3954 
3955 	If you change data programmatically, without going through the
3956 	DataControllerWidget methods, you will have to tell it something
3957 	has changed and it needs to redraw. This is done with the `invalidate`
3958 	method.
3959 
3960 	History:
3961 		Added Oct 28, 2020
3962 +/
3963 /// Group: generating_from_code
3964 class DataControllerWidget(T) : WidgetContainer {
3965 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
3966 		private alias Tref = T;
3967 	else
3968 		private alias Tref = T*;
3969 
3970 	Tref datum;
3971 
3972 	/++
3973 		See_also: [addDataControllerWidget]
3974 	+/
3975 	this(Tref datum, Widget parent) {
3976 		this.datum = datum;
3977 
3978 		Widget cp = this;
3979 
3980 		super(parent);
3981 
3982 		foreach(attr; __traits(getAttributes, T))
3983 			static if(is(typeof(attr) == ContainerMeta)) {
3984 				cp = attr.instantiate(this);
3985 			}
3986 
3987 		auto def = this.getByName("default");
3988 		if(def !is null)
3989 			cp = def;
3990 
3991 		Widget helper(string name) {
3992 			auto maybe = this.getByName(name);
3993 			if(maybe is null)
3994 				return cp;
3995 			return maybe;
3996 
3997 		}
3998 
3999 		foreach(member; __traits(allMembers, T))
4000 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4001 		static if(is(typeof(__traits(getMember, this.datum, member))))
4002 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4003 			void delegate() update;
4004 
4005 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4006 
4007 			if(update)
4008 				updaters ~= update;
4009 
4010 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4011 				w.addEventListener("triggered", delegate() {
4012 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))();
4013 					notifyDataUpdated();
4014 				});
4015 			} else static if(is(typeof(w.isChecked) == bool)) {
4016 				w.addEventListener(EventType.change, (Event ev) {
4017 					__traits(getMember, this.datum, member) = w.isChecked;
4018 				});
4019 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4020 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4021 			} else static if(is(typeof(w.value) == int)) {
4022 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4023 			} else static if(is(typeof(w) == DropDownSelection)) {
4024 				// special case for this to kinda support enums and such. coudl be better though
4025 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4026 			} else {
4027 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4028 			}
4029 		}
4030 	}
4031 
4032 	/++
4033 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4034 
4035 		History:
4036 			Added May 28, 2021
4037 	+/
4038 	void notifyDataUpdated() {
4039 		foreach(updater; updaters)
4040 			updater();
4041 
4042 		this.emit!(ChangeEvent!void)(delegate{});
4043 	}
4044 
4045 	private Widget[string] memberWidgets;
4046 	private void delegate()[] updaters;
4047 
4048 	mixin Emits!(ChangeEvent!void);
4049 }
4050 
4051 private int saturatedSum(int[] values...) {
4052 	int sum;
4053 	foreach(value; values) {
4054 		if(value == int.max)
4055 			return int.max;
4056 		sum += value;
4057 	}
4058 	return sum;
4059 }
4060 
4061 void genericSetValue(T, W)(T* where, W what) {
4062 	import std.conv;
4063 	*where = to!T(what);
4064 	//*where = cast(T) stringToLong(what);
4065 }
4066 
4067 /++
4068 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4069 
4070 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4071 
4072 	Note that this creates the widget but does not attach any event handlers to it.
4073 +/
4074 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4075 
4076 	string displayName = __traits(identifier, tt).beautify;
4077 
4078 	static if(controlledByCount!tt == 1) {
4079 		foreach(i, attr; __traits(getAttributes, tt)) {
4080 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4081 				auto w = attr.construct(parent);
4082 				static if(__traits(compiles, w.setPosition(*valptr)))
4083 					update = () { w.setPosition(*valptr); };
4084 				else static if(__traits(compiles, w.setValue(*valptr)))
4085 					update = () { w.setValue(*valptr); };
4086 
4087 				if(update)
4088 					update();
4089 				return w;
4090 			}
4091 		}
4092 	} else static if(controlledByCount!tt == 0) {
4093 		static if(is(typeof(tt) == enum)) {
4094 			// FIXME: update
4095 			auto dds = new DropDownSelection(parent);
4096 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4097 				dds.addOption(option);
4098 				if(__traits(getMember, typeof(tt), option) == *valptr)
4099 					dds.setSelection(cast(int) idx);
4100 			}
4101 			return dds;
4102 		} else static if(is(typeof(tt) == bool)) {
4103 			auto box = new Checkbox(displayName, parent);
4104 			update = () { box.isChecked = *valptr; };
4105 			update();
4106 			return box;
4107 		} else static if(is(typeof(tt) : const long)) {
4108 			auto le = new LabeledLineEdit(displayName, parent);
4109 			update = () { le.content = toInternal!string(*valptr); };
4110 			update();
4111 			return le;
4112 		} else static if(is(typeof(tt) : const double)) {
4113 			auto le = new LabeledLineEdit(displayName, parent);
4114 			import std.conv;
4115 			update = () { le.content = to!string(*valptr); };
4116 			update();
4117 			return le;
4118 		} else static if(is(typeof(tt) : const string)) {
4119 			auto le = new LabeledLineEdit(displayName, parent);
4120 			update = () { le.content = *valptr; };
4121 			update();
4122 			return le;
4123 		} else static if(is(typeof(tt) == function)) {
4124 			auto w = new Button(displayName, parent);
4125 			return w;
4126 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4127 			return parent.addDataControllerWidget(tt);
4128 		} else static assert(0, typeof(tt).stringof);
4129 	} else static assert(0, "multiple controllers not yet supported");
4130 }
4131 
4132 private template controlledByCount(alias tt) {
4133 	static int helper() {
4134 		int count;
4135 		foreach(i, attr; __traits(getAttributes, tt))
4136 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4137 				count++;
4138 		return count;
4139 	}
4140 
4141 	enum controlledByCount = helper;
4142 }
4143 
4144 /++
4145 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4146 
4147 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4148 
4149 	History:
4150 		The `redrawOnChange` parameter was added on May 28, 2021.
4151 +/
4152 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4153 	auto dcw = new DataControllerWidget!T(t, parent);
4154 	initializeDataControllerWidget(dcw, redrawOnChange);
4155 	return dcw;
4156 }
4157 
4158 /// ditto
4159 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4160 	auto dcw = new DataControllerWidget!T(t, parent);
4161 	initializeDataControllerWidget(dcw, redrawOnChange);
4162 	return dcw;
4163 }
4164 
4165 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4166 	if(redrawOnChange !is null)
4167 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4168 }
4169 
4170 /++
4171 	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.
4172 
4173 	History:
4174 		Finalized on June 3, 2021 for the dub v10.0 release
4175 +/
4176 struct StyleInformation {
4177 	private Widget w;
4178 	private BaseVisualTheme visualTheme;
4179 
4180 	private this(Widget w) {
4181 		this.w = w;
4182 		this.visualTheme = WidgetPainter.visualTheme;
4183 	}
4184 
4185 	/++
4186 		Forwards to [Widget.Style]
4187 
4188 		Bugs:
4189 			It is supposed to fall back to the [VisualTheme] if
4190 			the style doesn't override the default, but that is
4191 			not generally implemented. Many of them may end up
4192 			being explicit overloads instead of the generic
4193 			opDispatch fallback, like [font] is now.
4194 	+/
4195 	public @property opDispatch(string name)() {
4196 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4197 		w.useStyleProperties((scope Widget.Style props) {
4198 		//visualTheme.useStyleProperties(w, (props) {
4199 			prop = __traits(getMember, props, name);
4200 		});
4201 		return prop;
4202 	}
4203 
4204 	/++
4205 		Returns the cached font object associated with the widget,
4206 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4207 
4208 		History:
4209 			Prior to March 21, 2022 (dub v10.7), `font` went through
4210 			[opDispatch], which did not use the cache. You can now call it
4211 			repeatedly without guilt.
4212 	+/
4213 	public @property OperatingSystemFont font() {
4214 		OperatingSystemFont prop;
4215 		w.useStyleProperties((scope Widget.Style props) {
4216 			prop = props.fontCached;
4217 		});
4218 		if(prop is null) {
4219 			prop = visualTheme.defaultFontCached(w.currentDpi);
4220 		}
4221 		return prop;
4222 	}
4223 
4224 	@property {
4225 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4226 		/** */ int paddingLeft() { return w.paddingLeft(); }
4227 		/** */ int paddingRight() { return w.paddingRight(); }
4228 		/** */ int paddingTop() { return w.paddingTop(); }
4229 		/** */ int paddingBottom() { return w.paddingBottom(); }
4230 
4231 		/** */ int marginLeft() { return w.marginLeft(); }
4232 		/** */ int marginRight() { return w.marginRight(); }
4233 		/** */ int marginTop() { return w.marginTop(); }
4234 		/** */ int marginBottom() { return w.marginBottom(); }
4235 
4236 		/** */ int maxHeight() { return w.maxHeight(); }
4237 		/** */ int minHeight() { return w.minHeight(); }
4238 
4239 		/** */ int maxWidth() { return w.maxWidth(); }
4240 		/** */ int minWidth() { return w.minWidth(); }
4241 
4242 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4243 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4244 
4245 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4246 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4247 
4248 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4249 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4250 
4251 		// Global helpers some of these are unstable.
4252 		static:
4253 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4254 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4255 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4256 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4257 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4258 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4259 
4260 		/** */ Color activeTabColor() { return lightAccentColor; }
4261 		/** */ Color buttonColor() { return windowBackgroundColor; }
4262 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4263 		/** */ Color hoveringColor() { return lightAccentColor; }
4264 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4265 			auto c = WidgetPainter.visualTheme.selectionColor();
4266 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4267 		}
4268 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4269 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4270 	}
4271 
4272 
4273 
4274 	/+
4275 
4276 	private static auto extractStyleProperty(string name)(Widget w) {
4277 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4278 		w.useStyleProperties((props) {
4279 			prop = __traits(getMember, props, name);
4280 		});
4281 		return prop;
4282 	}
4283 
4284 	// FIXME: clear this upon a X server disconnect
4285 	private static OperatingSystemFont[string] fontCache;
4286 
4287 	T getProperty(T)(string name, lazy T default_) {
4288 		if(visualTheme !is null) {
4289 			auto str = visualTheme.getPropertyString(w, name);
4290 			if(str is null)
4291 				return default_;
4292 			static if(is(T == Color))
4293 				return Color.fromString(str);
4294 			else static if(is(T == Measurement))
4295 				return Measurement(cast(int) toInternal!int(str));
4296 			else static if(is(T == WidgetBackground))
4297 				return WidgetBackground.fromString(str);
4298 			else static if(is(T == OperatingSystemFont)) {
4299 				if(auto f = str in fontCache)
4300 					return *f;
4301 				else
4302 					return fontCache[str] = new OperatingSystemFont(str);
4303 			} else static if(is(T == FrameStyle)) {
4304 				switch(str) {
4305 					default:
4306 						return FrameStyle.none;
4307 					foreach(style; __traits(allMembers, FrameStyle))
4308 					case style:
4309 						return __traits(getMember, FrameStyle, style);
4310 				}
4311 			} else static assert(0);
4312 		} else
4313 			return default_;
4314 	}
4315 
4316 	static struct Measurement {
4317 		int value;
4318 		alias value this;
4319 	}
4320 
4321 	@property:
4322 
4323 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4324 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4325 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4326 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4327 
4328 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4329 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4330 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4331 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4332 
4333 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4334 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4335 
4336 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4337 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4338 
4339 
4340 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4341 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4342 
4343 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4344 
4345 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4346 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4347 
4348 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4349 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4350 
4351 
4352 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4353 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4354 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4355 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4356 
4357 	Color activeTabColor() { return lightAccentColor; }
4358 	Color buttonColor() { return windowBackgroundColor; }
4359 	Color depressedButtonColor() { return darkAccentColor; }
4360 	Color hoveringColor() { return Color(228, 228, 228); }
4361 	Color activeListXorColor() {
4362 		auto c = WidgetPainter.visualTheme.selectionColor();
4363 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4364 	}
4365 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4366 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4367 	+/
4368 }
4369 
4370 
4371 
4372 // pragma(msg, __traits(classInstanceSize, Widget));
4373 
4374 /*private*/ template EventString(E) {
4375 	static if(is(typeof(E.EventString)))
4376 		enum EventString = E.EventString;
4377 	else
4378 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4379 }
4380 
4381 /*private*/ template EventStringIdentifier(E) {
4382 	string helper() {
4383 		auto es = EventString!E;
4384 		char[] id = new char[](es.length * 2);
4385 		size_t idx;
4386 		foreach(char ch; es) {
4387 			id[idx++] = cast(char)('a' + (ch >> 4));
4388 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4389 		}
4390 		return cast(string) id;
4391 	}
4392 
4393 	enum EventStringIdentifier = helper();
4394 }
4395 
4396 
4397 template classStaticallyEmits(This, EventType) {
4398 	static if(is(This Base == super))
4399 		static if(is(Base : Widget))
4400 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4401 		else
4402 			enum baseEmits = false;
4403 	else
4404 		enum baseEmits = false;
4405 
4406 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
4407 
4408 	enum classStaticallyEmits = thisEmits || baseEmits;
4409 }
4410 
4411 /++
4412 	A helper to make widgets out of other native windows.
4413 
4414 	History:
4415 		Factored out of OpenGlWidget on November 5, 2021
4416 +/
4417 class NestedChildWindowWidget : Widget {
4418 	SimpleWindow win;
4419 
4420 	/++
4421 		Used on X to send focus to the appropriate child window when requested by the window manager.
4422 
4423 		Normally returns its own nested window. Can also return another child or null to revert to the parent
4424 		if you override it in a child class.
4425 
4426 		History:
4427 			Added April 2, 2022 (dub v10.8)
4428 	+/
4429 	SimpleWindow focusableWindow() {
4430 		return win;
4431 	}
4432 
4433 	///
4434 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4435 	this(SimpleWindow win, Widget parent) {
4436 		this.parentWindow = parent.parentWindow;
4437 		this.win = win;
4438 
4439 		super(parent);
4440 		windowsetup(win);
4441 	}
4442 
4443 	static protected SimpleWindow getParentWindow(Widget parent) {
4444 		assert(parent !is null);
4445 		SimpleWindow pwin = parent.parentWindow.win;
4446 
4447 		version(win32_widgets) {
4448 			HWND phwnd;
4449 			auto wtf = parent;
4450 			while(wtf) {
4451 				if(wtf.hwnd) {
4452 					phwnd = wtf.hwnd;
4453 					break;
4454 				}
4455 				wtf = wtf.parent;
4456 			}
4457 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
4458 			if(phwnd)
4459 				pwin = new SimpleWindow(phwnd);
4460 		}
4461 
4462 		return pwin;
4463 	}
4464 
4465 	/++
4466 		Called upon the nested window being destroyed.
4467 		Remember the window has already been destroyed at
4468 		this point, so don't use the native handle for anything.
4469 
4470 		History:
4471 			Added April 3, 2022 (dub v10.8)
4472 	+/
4473 	protected void dispose() {
4474 
4475 	}
4476 
4477 	protected void windowsetup(SimpleWindow w) {
4478 		/*
4479 		win.onFocusChange = (bool getting) {
4480 			if(getting)
4481 				this.focus();
4482 		};
4483 		*/
4484 
4485 		/+
4486 		win.onFocusChange = (bool getting) {
4487 			if(getting) {
4488 				this.parentWindow.focusedWidget = this;
4489 				this.emit!FocusEvent();
4490 				this.emit!FocusInEvent();
4491 			} else {
4492 				this.emit!BlurEvent();
4493 				this.emit!FocusOutEvent();
4494 			}
4495 		};
4496 		+/
4497 
4498 		win.onDestroyed = () {
4499 			this.dispose();
4500 		};
4501 
4502 		version(win32_widgets) {
4503 			Widget.nativeMapping[win.hwnd] = this;
4504 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4505 		} else {
4506 			win.setEventHandlers(
4507 				(MouseEvent e) {
4508 					Widget p = this;
4509 					while(p ! is parentWindow) {
4510 						e.x += p.x;
4511 						e.y += p.y;
4512 						p = p.parent;
4513 					}
4514 					parentWindow.dispatchMouseEvent(e);
4515 				},
4516 				(KeyEvent e) {
4517 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
4518 					parentWindow.dispatchKeyEvent(e);
4519 				},
4520 				(dchar e) {
4521 					parentWindow.dispatchCharEvent(e);
4522 				},
4523 			);
4524 		}
4525 
4526 	}
4527 
4528 	override void showing(bool s, bool recalc) {
4529 		auto cur = hidden;
4530 		win.hidden = !s;
4531 		if(cur != s && s)
4532 			redraw();
4533 	}
4534 
4535 	/// OpenGL widgets cannot have child widgets. Do not call this.
4536 	/* @disable */ final override void addChild(Widget, int) {
4537 		throw new Error("cannot add children to OpenGL widgets");
4538 	}
4539 
4540 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
4541 	/// Keep in mind that events like mouse coordinates are still relative to your size.
4542 	override void registerMovement() {
4543 		// writefln("%d %d %d %d", x,y,width,height);
4544 		version(win32_widgets)
4545 			auto pos = getChildPositionRelativeToParentHwnd(this);
4546 		else
4547 			auto pos = getChildPositionRelativeToParentOrigin(this);
4548 		win.moveResize(pos[0], pos[1], width, height);
4549 
4550 		registerMovementAdditionalWork();
4551 		sendResizeEvent();
4552 	}
4553 
4554 	abstract void registerMovementAdditionalWork();
4555 }
4556 
4557 /++
4558 	Nests an opengl capable window inside this window as a widget.
4559 
4560 	You may also just want to create an additional [SimpleWindow] with
4561 	[OpenGlOptions.yes] yourself.
4562 
4563 	An OpenGL widget cannot have child widgets. It will throw if you try.
4564 +/
4565 static if(OpenGlEnabled)
4566 class OpenGlWidget : NestedChildWindowWidget {
4567 
4568 	override void registerMovementAdditionalWork() {
4569 		win.setAsCurrentOpenGlContext();
4570 	}
4571 
4572 	///
4573 	this(Widget parent) {
4574 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4575 		super(win, parent);
4576 	}
4577 
4578 	override void paint(WidgetPainter painter) {
4579 		win.setAsCurrentOpenGlContext();
4580 		glViewport(0, 0, this.width, this.height);
4581 		win.redrawOpenGlSceneNow();
4582 	}
4583 
4584 	void redrawOpenGlScene(void delegate() dg) {
4585 		win.redrawOpenGlScene = dg;
4586 	}
4587 }
4588 
4589 /++
4590 	This demo shows how to draw text in an opengl scene.
4591 +/
4592 unittest {
4593 	import arsd.minigui;
4594 	import arsd.ttf;
4595 
4596 	void main() {
4597 		auto window = new Window();
4598 
4599 		auto widget = new OpenGlWidget(window);
4600 
4601 		// old means non-shader code so compatible with glBegin etc.
4602 		// tbh I haven't implemented new one in font yet...
4603 		// anyway, declaring here, will construct soon.
4604 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
4605 
4606 		// this is a little bit awkward, calling some methods through
4607 		// the underlying SimpleWindow `win` method, and you can't do this
4608 		// on a nanovega widget due to conflicts so I should probably fix
4609 		// the api to be a bit easier. But here it will work.
4610 		//
4611 		// Alternatively, you could load the font on the first draw, inside
4612 		// the redrawOpenGlScene, and keep a flag so you don't do it every
4613 		// time. That'd be a bit easier since the lib sets up the context
4614 		// by then guaranteed.
4615 		//
4616 		// But still, I wanna show this.
4617 		widget.win.visibleForTheFirstTime = delegate {
4618 			// must set the opengl context
4619 			widget.win.setAsCurrentOpenGlContext();
4620 
4621 			// if you were doing a OpenGL 3+ shader, this
4622 			// gets especially important to do in order. With
4623 			// old-style opengl, I think you can even do it
4624 			// in main(), but meh, let's show it more correctly.
4625 
4626 			// Anyway, now it is time to load the font from the
4627 			// OS (you can alternatively load one from a .ttf file
4628 			// you bundle with the application), then load the
4629 			// font into texture for drawing.
4630 
4631 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
4632 
4633 			assert(!osfont.isNull()); // make sure it actually loaded
4634 
4635 			// using typeof to avoid repeating the long name lol
4636 			glfont = new typeof(glfont)(
4637 				// get the raw data from the font for loading in here
4638 				// since it doesn't use the OS function to draw the
4639 				// text, we gotta treat it more as a file than as
4640 				// a drawing api.
4641 				osfont.getTtfBytes(),
4642 				18, // need to respecify size since opengl world is different coordinate system
4643 
4644 				// these last two numbers are why it is called
4645 				// "Limited" font. It only loads the characters
4646 				// in the given range, since the texture atlas
4647 				// it references is all a big image generated ahead
4648 				// of time. You could maybe do the whole thing but
4649 				// idk how much memory that is.
4650 				//
4651 				// But here, 0-128 represents the ASCII range, so
4652 				// good enough for most English things, numeric labels,
4653 				// etc.
4654 				0,
4655 				128
4656 			);
4657 		};
4658 
4659 		widget.redrawOpenGlScene = () {
4660 			// now we can use the glfont's drawString function
4661 
4662 			// first some opengl setup. You can do this in one place
4663 			// on window first visible too in many cases, just showing
4664 			// here cuz it is easier for me.
4665 
4666 			// gonna need some alpha blending or it just looks awful
4667 			glEnable(GL_BLEND);
4668 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
4669 			glClearColor(0,0,0,0);
4670 			glDepthFunc(GL_LEQUAL);
4671 
4672 			// Also need to enable 2d textures, since it draws the
4673 			// font characters as images baked in
4674 			glMatrixMode(GL_MODELVIEW);
4675 			glLoadIdentity();
4676 			glDisable(GL_DEPTH_TEST);
4677 			glEnable(GL_TEXTURE_2D);
4678 
4679 			// the orthographic matrix is best for 2d things like text
4680 			// so let's set that up. This matrix makes the coordinates
4681 			// in the opengl scene be one-to-one with the actual pixels
4682 			// on screen. (Not necessarily best, you may wish to scale
4683 			// things, but it does help keep fonts looking normal.)
4684 			glMatrixMode(GL_PROJECTION);
4685 			glLoadIdentity();
4686 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
4687 
4688 			// you can do other glScale, glRotate, glTranslate, etc
4689 			// to the matrix here of course if you want.
4690 
4691 			// note the x,y coordinates here are for the text baseline
4692 			// NOT the upper-left corner. The baseline is like the line
4693 			// in the notebook you write on. Most the letters are actually
4694 			// above it, but some, like p and q, dip a bit below it.
4695 			//
4696 			// So if you're used to the upper left coordinate like the
4697 			// rest of simpledisplay/minigui usually do, do the
4698 			// y + glfont.ascent to bring it down a little. So this
4699 			// example puts the string in the upper left of the window.
4700 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
4701 
4702 			// re color btw: the function sets a solid color internally,
4703 			// but you actually COULD do your own thing for rainbow effects
4704 			// and the sort if you wanted too, by pulling its guts out.
4705 			// Just view its source for an idea of how it actually draws:
4706 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
4707 
4708 			// it gets a bit complicated with the character positioning,
4709 			// but the opengl parts are fairly simple: bind a texture,
4710 			// set the color, draw a quad for each letter.
4711 
4712 
4713 			// the last optional argument there btw is a bounding box
4714 			// it will/ use to word wrap and return an object you can
4715 			// use to implement scrolling or pagination; it tells how
4716 			// much of the string didn't fit in the box. But for simple
4717 			// labels we can just ignore that.
4718 
4719 
4720 			// I'd suggest drawing text as the last step, after you
4721 			// do your other drawing. You might use the push/pop matrix
4722 			// stuff to keep your place. You, in theory, should be able
4723 			// to do text in a 3d space but I've never actually tried
4724 			// that....
4725 		};
4726 
4727 		window.loop();
4728 	}
4729 }
4730 
4731 version(custom_widgets)
4732 	private alias ListWidgetBase = ScrollableWidget;
4733 else
4734 	private alias ListWidgetBase = Widget;
4735 
4736 /++
4737 	A list widget contains a list of strings that the user can examine and select.
4738 
4739 
4740 	In the future, items in the list may be possible to be more than just strings.
4741 
4742 	See_Also:
4743 		[TableView]
4744 +/
4745 class ListWidget : ListWidgetBase {
4746 	/// 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.
4747 	mixin Emits!(ChangeEvent!void);
4748 
4749 	static struct Option {
4750 		string label;
4751 		bool selected;
4752 		void* tag;
4753 	}
4754 
4755 	/++
4756 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
4757 	+/
4758 	void setSelection(int y) {
4759 		if(!multiSelect)
4760 			foreach(ref opt; options)
4761 				opt.selected = false;
4762 		if(y >= 0 && y < options.length)
4763 			options[y].selected = !options[y].selected;
4764 
4765 		this.emit!(ChangeEvent!void)(delegate {});
4766 
4767 		version(custom_widgets)
4768 			redraw();
4769 	}
4770 
4771 	/++
4772 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
4773 		Returns -1 if nothing is selected.
4774 	+/
4775 	int getSelection()
4776 	{
4777 		foreach(i, opt; options) {
4778 			if (opt.selected)
4779 				return cast(int) i;
4780 		}
4781 		return -1;
4782 	}
4783 
4784 	version(custom_widgets)
4785 	override void defaultEventHandler_click(ClickEvent event) {
4786 		this.focus();
4787 		if(event.button == MouseButton.left) {
4788 			auto y = (event.clientY - 4) / defaultLineHeight;
4789 			if(y >= 0 && y < options.length) {
4790 				setSelection(y);
4791 			}
4792 		}
4793 		super.defaultEventHandler_click(event);
4794 	}
4795 
4796 	this(Widget parent) {
4797 		tabStop = false;
4798 		super(parent);
4799 		version(win32_widgets)
4800 			createWin32Window(this, WC_LISTBOX, "",
4801 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
4802 	}
4803 
4804 	version(win32_widgets)
4805 	override void handleWmCommand(ushort code, ushort id) {
4806 		switch(code) {
4807 			case LBN_SELCHANGE:
4808 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
4809 				setSelection(cast(int) sel);
4810 			break;
4811 			default:
4812 		}
4813 	}
4814 
4815 
4816 	version(custom_widgets)
4817 	override void paintFrameAndBackground(WidgetPainter painter) {
4818 		draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor);
4819 	}
4820 
4821 	version(custom_widgets)
4822 	override void paint(WidgetPainter painter) {
4823 		auto cs = getComputedStyle();
4824 		auto pos = Point(4, 4);
4825 		foreach(idx, option; options) {
4826 			painter.fillColor = painter.visualTheme.widgetBackgroundColor;
4827 			painter.outlineColor = painter.visualTheme.widgetBackgroundColor;
4828 			painter.drawRectangle(pos, width - 8, defaultLineHeight);
4829 			if(option.selected) {
4830 				//painter.rasterOp = RasterOp.xor;
4831 				painter.outlineColor = cs.selectionForegroundColor;
4832 				painter.fillColor = cs.selectionBackgroundColor;
4833 				painter.drawRectangle(pos, width - 8, defaultLineHeight);
4834 				//painter.rasterOp = RasterOp.normal;
4835 			}
4836 			painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor;
4837 			painter.drawText(pos, option.label);
4838 			pos.y += defaultLineHeight;
4839 		}
4840 	}
4841 
4842 	static class Style : Widget.Style {
4843 		override WidgetBackground background() {
4844 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
4845 		}
4846 	}
4847 	mixin OverrideStyle!Style;
4848 	//mixin Padding!q{2};
4849 
4850 	void addOption(string text, void* tag = null) {
4851 		options ~= Option(text, false, tag);
4852 		version(win32_widgets) {
4853 			WCharzBuffer buffer = WCharzBuffer(text);
4854 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
4855 		}
4856 		version(custom_widgets) {
4857 			setContentSize(width, cast(int) (options.length * defaultLineHeight));
4858 			redraw();
4859 		}
4860 	}
4861 
4862 	void clear() {
4863 		options = null;
4864 		version(win32_widgets) {
4865 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
4866 				{}
4867 
4868 		} else version(custom_widgets) {
4869 			scrollTo(Point(0, 0));
4870 			redraw();
4871 		}
4872 	}
4873 
4874 	Option[] options;
4875 	version(win32_widgets)
4876 		enum multiSelect = false; /// not implemented yet
4877 	else
4878 		bool multiSelect;
4879 
4880 	override int heightStretchiness() { return 6; }
4881 }
4882 
4883 
4884 
4885 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
4886 enum ScrollBarShowPolicy {
4887 	automatic, /// automatically show the scroll bar if it is necessary
4888 	never, /// never show the scroll bar (scrolling must be done programmatically)
4889 	always /// always show the scroll bar, even if it is disabled
4890 }
4891 
4892 /++
4893 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
4894 
4895 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
4896 +/
4897 // FIXME ScrollBarShowPolicy
4898 // FIXME: use the ScrollMessageWidget in here now that it exists
4899 class ScrollableWidget : Widget {
4900 	// FIXME: make line size configurable
4901 	// FIXME: add keyboard controls
4902 	version(win32_widgets) {
4903 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
4904 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
4905 				auto pos = HIWORD(wParam);
4906 				auto m = LOWORD(wParam);
4907 
4908 				// FIXME: I can reintroduce the
4909 				// scroll bars now by using this
4910 				// in the top-level window handler
4911 				// to forward comamnds
4912 				auto scrollbarHwnd = lParam;
4913 				switch(m) {
4914 					case SB_BOTTOM:
4915 						if(msg == WM_HSCROLL)
4916 							horizontalScrollTo(contentWidth_);
4917 						else
4918 							verticalScrollTo(contentHeight_);
4919 					break;
4920 					case SB_TOP:
4921 						if(msg == WM_HSCROLL)
4922 							horizontalScrollTo(0);
4923 						else
4924 							verticalScrollTo(0);
4925 					break;
4926 					case SB_ENDSCROLL:
4927 						// idk
4928 					break;
4929 					case SB_LINEDOWN:
4930 						if(msg == WM_HSCROLL)
4931 							horizontalScroll(scaleWithDpi(16));
4932 						else
4933 							verticalScroll(scaleWithDpi(16));
4934 					break;
4935 					case SB_LINEUP:
4936 						if(msg == WM_HSCROLL)
4937 							horizontalScroll(scaleWithDpi(-16));
4938 						else
4939 							verticalScroll(scaleWithDpi(-16));
4940 					break;
4941 					case SB_PAGEDOWN:
4942 						if(msg == WM_HSCROLL)
4943 							horizontalScroll(scaleWithDpi(100));
4944 						else
4945 							verticalScroll(scaleWithDpi(100));
4946 					break;
4947 					case SB_PAGEUP:
4948 						if(msg == WM_HSCROLL)
4949 							horizontalScroll(scaleWithDpi(-100));
4950 						else
4951 							verticalScroll(scaleWithDpi(-100));
4952 					break;
4953 					case SB_THUMBPOSITION:
4954 					case SB_THUMBTRACK:
4955 						if(msg == WM_HSCROLL)
4956 							horizontalScrollTo(pos);
4957 						else
4958 							verticalScrollTo(pos);
4959 
4960 						if(m == SB_THUMBTRACK) {
4961 							// the event loop doesn't seem to carry on with a requested redraw..
4962 							// so we request it to get our dirty bit set...
4963 							redraw();
4964 
4965 							// then we need to immediately actually redraw it too for instant feedback to user
4966 
4967 							SimpleWindow.processAllCustomEvents();
4968 							//if(parentWindow)
4969 								//parentWindow.actualRedraw();
4970 						}
4971 					break;
4972 					default:
4973 				}
4974 			}
4975 			return super.hookedWndProc(msg, wParam, lParam);
4976 		}
4977 	}
4978 	///
4979 	this(Widget parent) {
4980 		this.parentWindow = parent.parentWindow;
4981 
4982 		version(win32_widgets) {
4983 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
4984 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
4985 			super(parent);
4986 		} else version(custom_widgets) {
4987 			outerContainer = new InternalScrollableContainerWidget(this, parent);
4988 			super(outerContainer);
4989 		} else static assert(0);
4990 	}
4991 
4992 	version(custom_widgets)
4993 		InternalScrollableContainerWidget outerContainer;
4994 
4995 	override void defaultEventHandler_click(ClickEvent event) {
4996 		if(event.button == MouseButton.wheelUp)
4997 			verticalScroll(scaleWithDpi(-16));
4998 		if(event.button == MouseButton.wheelDown)
4999 			verticalScroll(scaleWithDpi(16));
5000 		super.defaultEventHandler_click(event);
5001 	}
5002 
5003 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5004 		switch(event.key) {
5005 			case Key.Left:
5006 				horizontalScroll(scaleWithDpi(-16));
5007 			break;
5008 			case Key.Right:
5009 				horizontalScroll(scaleWithDpi(16));
5010 			break;
5011 			case Key.Up:
5012 				verticalScroll(scaleWithDpi(-16));
5013 			break;
5014 			case Key.Down:
5015 				verticalScroll(scaleWithDpi(16));
5016 			break;
5017 			case Key.Home:
5018 				verticalScrollTo(0);
5019 			break;
5020 			case Key.End:
5021 				verticalScrollTo(contentHeight);
5022 			break;
5023 			case Key.PageUp:
5024 				verticalScroll(scaleWithDpi(-160));
5025 			break;
5026 			case Key.PageDown:
5027 				verticalScroll(scaleWithDpi(160));
5028 			break;
5029 			default:
5030 		}
5031 		super.defaultEventHandler_keydown(event);
5032 	}
5033 
5034 
5035 	version(win32_widgets)
5036 	override void recomputeChildLayout() {
5037 		super.recomputeChildLayout();
5038 		SCROLLINFO info;
5039 		info.cbSize = info.sizeof;
5040 		info.nPage = viewportHeight;
5041 		info.fMask = SIF_PAGE | SIF_RANGE;
5042 		info.nMin = 0;
5043 		info.nMax = contentHeight_;
5044 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5045 
5046 		info.cbSize = info.sizeof;
5047 		info.nPage = viewportWidth;
5048 		info.fMask = SIF_PAGE | SIF_RANGE;
5049 		info.nMin = 0;
5050 		info.nMax = contentWidth_;
5051 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5052 	}
5053 
5054 	/*
5055 		Scrolling
5056 		------------
5057 
5058 		You are assigned a width and a height by the layout engine, which
5059 		is your viewport box. However, you may draw more than that by setting
5060 		a contentWidth and contentHeight.
5061 
5062 		If these can be contained by the viewport, no scrollbar is displayed.
5063 		If they cannot fit though, it will automatically show scroll as necessary.
5064 
5065 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5066 		is zero, no vertical scrolling is performed.
5067 
5068 		If scrolling is necessary, the lib will automatically work with the bars.
5069 		When you redraw, the origin and clipping info in the painter is set so if
5070 		you just draw everything, it will work, but you can be more efficient by checking
5071 		the viewportWidth, viewportHeight, and scrollOrigin members.
5072 	*/
5073 
5074 	///
5075 	final @property int viewportWidth() {
5076 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5077 	}
5078 	///
5079 	final @property int viewportHeight() {
5080 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5081 	}
5082 
5083 	// FIXME property
5084 	Point scrollOrigin_;
5085 
5086 	///
5087 	final const(Point) scrollOrigin() {
5088 		return scrollOrigin_;
5089 	}
5090 
5091 	// the user sets these two
5092 	private int contentWidth_ = 0;
5093 	private int contentHeight_ = 0;
5094 
5095 	///
5096 	int contentWidth() { return contentWidth_; }
5097 	///
5098 	int contentHeight() { return contentHeight_; }
5099 
5100 	///
5101 	void setContentSize(int width, int height) {
5102 		contentWidth_ = width;
5103 		contentHeight_ = height;
5104 
5105 		version(custom_widgets) {
5106 			if(showingVerticalScroll || showingHorizontalScroll) {
5107 				outerContainer.recomputeChildLayout();
5108 			}
5109 
5110 			if(showingVerticalScroll())
5111 				outerContainer.verticalScrollBar.redraw();
5112 			if(showingHorizontalScroll())
5113 				outerContainer.horizontalScrollBar.redraw();
5114 		} else version(win32_widgets) {
5115 			recomputeChildLayout();
5116 		} else static assert(0);
5117 	}
5118 
5119 	///
5120 	void verticalScroll(int delta) {
5121 		verticalScrollTo(scrollOrigin.y + delta);
5122 	}
5123 	///
5124 	void verticalScrollTo(int pos) {
5125 		scrollOrigin_.y = pos;
5126 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5127 			scrollOrigin_.y = contentHeight - viewportHeight;
5128 
5129 		if(scrollOrigin_.y < 0)
5130 			scrollOrigin_.y = 0;
5131 
5132 		version(win32_widgets) {
5133 			SCROLLINFO info;
5134 			info.cbSize = info.sizeof;
5135 			info.fMask = SIF_POS;
5136 			info.nPos = scrollOrigin_.y;
5137 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5138 		} else version(custom_widgets) {
5139 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5140 		} else static assert(0);
5141 
5142 		redraw();
5143 	}
5144 
5145 	///
5146 	void horizontalScroll(int delta) {
5147 		horizontalScrollTo(scrollOrigin.x + delta);
5148 	}
5149 	///
5150 	void horizontalScrollTo(int pos) {
5151 		scrollOrigin_.x = pos;
5152 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5153 			scrollOrigin_.x = contentWidth - viewportWidth;
5154 
5155 		if(scrollOrigin_.x < 0)
5156 			scrollOrigin_.x = 0;
5157 
5158 		version(win32_widgets) {
5159 			SCROLLINFO info;
5160 			info.cbSize = info.sizeof;
5161 			info.fMask = SIF_POS;
5162 			info.nPos = scrollOrigin_.x;
5163 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5164 		} else version(custom_widgets) {
5165 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5166 		} else static assert(0);
5167 
5168 		redraw();
5169 	}
5170 	///
5171 	void scrollTo(Point p) {
5172 		verticalScrollTo(p.y);
5173 		horizontalScrollTo(p.x);
5174 	}
5175 
5176 	///
5177 	void ensureVisibleInScroll(Point p) {
5178 		auto rect = viewportRectangle();
5179 		if(rect.contains(p))
5180 			return;
5181 		if(p.x < rect.left)
5182 			horizontalScroll(p.x - rect.left);
5183 		else if(p.x > rect.right)
5184 			horizontalScroll(p.x - rect.right);
5185 
5186 		if(p.y < rect.top)
5187 			verticalScroll(p.y - rect.top);
5188 		else if(p.y > rect.bottom)
5189 			verticalScroll(p.y - rect.bottom);
5190 	}
5191 
5192 	///
5193 	void ensureVisibleInScroll(Rectangle rect) {
5194 		ensureVisibleInScroll(rect.upperLeft);
5195 		ensureVisibleInScroll(rect.lowerRight);
5196 	}
5197 
5198 	///
5199 	Rectangle viewportRectangle() {
5200 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5201 	}
5202 
5203 	///
5204 	bool showingHorizontalScroll() {
5205 		return contentWidth > width;
5206 	}
5207 	///
5208 	bool showingVerticalScroll() {
5209 		return contentHeight > height;
5210 	}
5211 
5212 	/// This is called before the ordinary paint delegate,
5213 	/// giving you a chance to draw the window frame, etc,
5214 	/// before the scroll clip takes effect
5215 	void paintFrameAndBackground(WidgetPainter painter) {
5216 		version(win32_widgets) {
5217 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5218 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5219 			// since the pen is null, to fill the whole space, we need the +1 on both.
5220 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5221 			SelectObject(painter.impl.hdc, p);
5222 			SelectObject(painter.impl.hdc, b);
5223 		}
5224 
5225 	}
5226 
5227 	// make space for the scroll bar, and that's it.
5228 	final override int paddingRight() { return scaleWithDpi(16); }
5229 	final override int paddingBottom() { return scaleWithDpi(16); }
5230 
5231 	/*
5232 		END SCROLLING
5233 	*/
5234 
5235 	override WidgetPainter draw() {
5236 		int x = this.x, y = this.y;
5237 		auto parent = this.parent;
5238 		while(parent) {
5239 			x += parent.x;
5240 			y += parent.y;
5241 			parent = parent.parent;
5242 		}
5243 
5244 		//version(win32_widgets) {
5245 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5246 		//} else {
5247 			auto painter = parentWindow.win.draw(true);
5248 		//}
5249 		painter.originX = x;
5250 		painter.originY = y;
5251 
5252 		painter.originX = painter.originX - scrollOrigin.x;
5253 		painter.originY = painter.originY - scrollOrigin.y;
5254 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
5255 
5256 		return WidgetPainter(painter, this);
5257 	}
5258 
5259 	mixin ScrollableChildren;
5260 }
5261 
5262 // you need to have a Point scrollOrigin in the class somewhere
5263 // and a paintFrameAndBackground
5264 private mixin template ScrollableChildren() {
5265 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5266 		if(hidden)
5267 			return;
5268 
5269 		//version(win32_widgets)
5270 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5271 
5272 		painter.originX = lox + x;
5273 		painter.originY = loy + y;
5274 
5275 		bool actuallyPainted = false;
5276 
5277 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
5278 		if(clip == Rectangle.init)
5279 			return;
5280 
5281 		if(force || redrawRequested) {
5282 			//painter.setClipRectangle(scrollOrigin, width, height);
5283 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5284 			paintFrameAndBackground(painter);
5285 		}
5286 
5287 		painter.originX = painter.originX - scrollOrigin.x;
5288 		painter.originY = painter.originY - scrollOrigin.y;
5289 		if(force || redrawRequested) {
5290 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
5291 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
5292 
5293 			//erase(painter); // we paintFrameAndBackground above so no need
5294 			if(painter.visualTheme)
5295 				painter.visualTheme.doPaint(this, painter);
5296 			else
5297 				paint(painter);
5298 
5299 			if(invalidate) {
5300 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5301 				// children are contained inside this, so no need to do extra work
5302 				invalidate = false;
5303 			}
5304 
5305 
5306 			actuallyPainted = true;
5307 			redrawRequested = false;
5308 		}
5309 		foreach(child; children) {
5310 			if(cast(FixedPosition) child)
5311 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5312 			else
5313 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5314 		}
5315 	}
5316 }
5317 
5318 private class InternalScrollableContainerInsideWidget : ContainerWidget {
5319 	ScrollableContainerWidget scw;
5320 
5321 	this(ScrollableContainerWidget parent) {
5322 		scw = parent;
5323 		super(parent);
5324 	}
5325 
5326 	version(custom_widgets)
5327 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5328 		if(hidden)
5329 			return;
5330 
5331 		bool actuallyPainted = false;
5332 
5333 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
5334 
5335 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
5336 		if(clip == Rectangle.init)
5337 			return;
5338 
5339 		painter.originX = lox + x - scrollOrigin.x;
5340 		painter.originY = loy + y - scrollOrigin.y;
5341 		if(force || redrawRequested) {
5342 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5343 
5344 			erase(painter);
5345 			if(painter.visualTheme)
5346 				painter.visualTheme.doPaint(this, painter);
5347 			else
5348 				paint(painter);
5349 
5350 			if(invalidate) {
5351 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5352 				// children are contained inside this, so no need to do extra work
5353 				invalidate = false;
5354 			}
5355 
5356 			actuallyPainted = true;
5357 			redrawRequested = false;
5358 		}
5359 		foreach(child; children) {
5360 			if(cast(FixedPosition) child)
5361 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5362 			else
5363 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5364 		}
5365 	}
5366 
5367 	version(custom_widgets)
5368 	override protected void addScrollPosition(ref int x, ref int y) {
5369 		x += scw.scrollX_;
5370 		y += scw.scrollY_;
5371 	}
5372 }
5373 
5374 /++
5375 	A widget meant to contain other widgets that may need to scroll.
5376 
5377 	Currently buggy.
5378 
5379 	History:
5380 		Added July 1, 2021 (dub v10.2)
5381 
5382 		On January 3, 2022, I tried to use it in a few other cases
5383 		and found it only worked well in the original test case. Since
5384 		it still sucks, I think I'm going to rewrite it again.
5385 +/
5386 class ScrollableContainerWidget : ContainerWidget {
5387 	///
5388 	this(Widget parent) {
5389 		super(parent);
5390 
5391 		container = new InternalScrollableContainerInsideWidget(this);
5392 		hsb = new HorizontalScrollbar(this);
5393 		vsb = new VerticalScrollbar(this);
5394 
5395 		tabStop = false;
5396 		container.tabStop = false;
5397 		magic = true;
5398 
5399 
5400 		vsb.addEventListener("scrolltonextline", () {
5401 			scrollBy(0, scaleWithDpi(16));
5402 		});
5403 		vsb.addEventListener("scrolltopreviousline", () {
5404 			scrollBy(0,scaleWithDpi( -16));
5405 		});
5406 		vsb.addEventListener("scrolltonextpage", () {
5407 			scrollBy(0, container.height);
5408 		});
5409 		vsb.addEventListener("scrolltopreviouspage", () {
5410 			scrollBy(0, -container.height);
5411 		});
5412 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
5413 			scrollTo(scrollX_, spe.value);
5414 		});
5415 
5416 		this.addEventListener(delegate (scope ClickEvent e) {
5417 			if(e.button == MouseButton.wheelUp) {
5418 				if(!e.defaultPrevented)
5419 					scrollBy(0, scaleWithDpi(-16));
5420 				e.stopPropagation();
5421 			} else if(e.button == MouseButton.wheelDown) {
5422 				if(!e.defaultPrevented)
5423 					scrollBy(0, scaleWithDpi(16));
5424 				e.stopPropagation();
5425 			}
5426 		});
5427 	}
5428 
5429 	/+
5430 	override void defaultEventHandler_click(ClickEvent e) {
5431 	}
5432 	+/
5433 
5434 	override void removeAllChildren() {
5435 		container.removeAllChildren();
5436 	}
5437 
5438 	void scrollTo(int x, int y) {
5439 		scrollBy(x - scrollX_, y - scrollY_);
5440 	}
5441 
5442 	void scrollBy(int x, int y) {
5443 		auto ox = scrollX_;
5444 		auto oy = scrollY_;
5445 
5446 		auto nx = ox + x;
5447 		auto ny = oy + y;
5448 
5449 		if(nx < 0)
5450 			nx = 0;
5451 		if(ny < 0)
5452 			ny = 0;
5453 
5454 		auto maxX = hsb.max - container.width;
5455 		if(maxX < 0) maxX = 0;
5456 		auto maxY = vsb.max - container.height;
5457 		if(maxY < 0) maxY = 0;
5458 
5459 		if(nx > maxX)
5460 			nx = maxX;
5461 		if(ny > maxY)
5462 			ny = maxY;
5463 
5464 		auto dx = nx - ox;
5465 		auto dy = ny - oy;
5466 
5467 		if(dx || dy) {
5468 			version(win32_widgets)
5469 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
5470 			else {
5471 				redraw();
5472 			}
5473 
5474 			hsb.setPosition = nx;
5475 			vsb.setPosition = ny;
5476 
5477 			scrollX_ = nx;
5478 			scrollY_ = ny;
5479 		}
5480 	}
5481 
5482 	private int scrollX_;
5483 	private int scrollY_;
5484 
5485 	void setTotalArea(int width, int height) {
5486 		hsb.setMax(width);
5487 		vsb.setMax(height);
5488 	}
5489 
5490 	///
5491 	void setViewableArea(int width, int height) {
5492 		hsb.setViewableArea(width);
5493 		vsb.setViewableArea(height);
5494 	}
5495 
5496 	private bool magic;
5497 	override void addChild(Widget w, int position = int.max) {
5498 		if(magic)
5499 			container.addChild(w, position);
5500 		else
5501 			super.addChild(w, position);
5502 	}
5503 
5504 	override void recomputeChildLayout() {
5505 		if(hsb is null || vsb is null || container is null) return;
5506 
5507 		/+
5508 		writeln(x, " ", y , " ", width, " ", height);
5509 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
5510 		+/
5511 
5512 		registerMovement();
5513 
5514 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
5515 		hsb.x = 0;
5516 		hsb.y = this.height - hsb.height;
5517 		hsb.width = this.width - scaleWithDpi(16);
5518 		hsb.recomputeChildLayout();
5519 
5520 		vsb.width = scaleWithDpi(16); // FIXME?
5521 		vsb.x = this.width - vsb.width;
5522 		vsb.y = 0;
5523 		vsb.height = this.height - scaleWithDpi(16);
5524 		vsb.recomputeChildLayout();
5525 
5526 		container.x = 0;
5527 		container.y = 0;
5528 		container.width = this.width - vsb.width;
5529 		container.height = this.height - hsb.height;
5530 		container.recomputeChildLayout();
5531 
5532 		scrollX_ = 0;
5533 		scrollY_ = 0;
5534 
5535 		hsb.setPosition(0);
5536 		vsb.setPosition(0);
5537 
5538 		int mw, mh;
5539 		Widget c = container;
5540 		// FIXME: hack here to handle a layout inside...
5541 		if(c.children.length == 1 && cast(Layout) c.children[0])
5542 			c = c.children[0];
5543 		foreach(child; c.children) {
5544 			auto w = child.x + child.width;
5545 			auto h = child.y + child.height;
5546 
5547 			if(w > mw) mw = w;
5548 			if(h > mh) mh = h;
5549 		}
5550 
5551 		setTotalArea(mw, mh);
5552 		setViewableArea(width, height);
5553 	}
5554 
5555 	override int minHeight() { return scaleWithDpi(64); }
5556 
5557 	HorizontalScrollbar hsb;
5558 	VerticalScrollbar vsb;
5559 	ContainerWidget container;
5560 }
5561 
5562 
5563 version(custom_widgets)
5564 private class InternalScrollableContainerWidget : Widget {
5565 
5566 	ScrollableWidget sw;
5567 
5568 	VerticalScrollbar verticalScrollBar;
5569 	HorizontalScrollbar horizontalScrollBar;
5570 
5571 	this(ScrollableWidget sw, Widget parent) {
5572 		this.sw = sw;
5573 
5574 		this.tabStop = false;
5575 
5576 		super(parent);
5577 
5578 		horizontalScrollBar = new HorizontalScrollbar(this);
5579 		verticalScrollBar = new VerticalScrollbar(this);
5580 
5581 		horizontalScrollBar.showing_ = false;
5582 		verticalScrollBar.showing_ = false;
5583 
5584 		horizontalScrollBar.addEventListener("scrolltonextline", {
5585 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
5586 			sw.horizontalScrollTo(horizontalScrollBar.position);
5587 		});
5588 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
5589 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
5590 			sw.horizontalScrollTo(horizontalScrollBar.position);
5591 		});
5592 		verticalScrollBar.addEventListener("scrolltonextline", {
5593 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
5594 			sw.verticalScrollTo(verticalScrollBar.position);
5595 		});
5596 		verticalScrollBar.addEventListener("scrolltopreviousline", {
5597 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
5598 			sw.verticalScrollTo(verticalScrollBar.position);
5599 		});
5600 		horizontalScrollBar.addEventListener("scrolltonextpage", {
5601 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
5602 			sw.horizontalScrollTo(horizontalScrollBar.position);
5603 		});
5604 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
5605 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
5606 			sw.horizontalScrollTo(horizontalScrollBar.position);
5607 		});
5608 		verticalScrollBar.addEventListener("scrolltonextpage", {
5609 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
5610 			sw.verticalScrollTo(verticalScrollBar.position);
5611 		});
5612 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
5613 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
5614 			sw.verticalScrollTo(verticalScrollBar.position);
5615 		});
5616 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
5617 			horizontalScrollBar.setPosition(event.intValue);
5618 			sw.horizontalScrollTo(horizontalScrollBar.position);
5619 		});
5620 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
5621 			verticalScrollBar.setPosition(event.intValue);
5622 			sw.verticalScrollTo(verticalScrollBar.position);
5623 		});
5624 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
5625 			horizontalScrollBar.setPosition(event.intValue);
5626 			sw.horizontalScrollTo(horizontalScrollBar.position);
5627 		});
5628 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
5629 			verticalScrollBar.setPosition(event.intValue);
5630 		});
5631 	}
5632 
5633 	// this is supposed to be basically invisible...
5634 	override int minWidth() { return sw.minWidth; }
5635 	override int minHeight() { return sw.minHeight; }
5636 	override int maxWidth() { return sw.maxWidth; }
5637 	override int maxHeight() { return sw.maxHeight; }
5638 	override int widthStretchiness() { return sw.widthStretchiness; }
5639 	override int heightStretchiness() { return sw.heightStretchiness; }
5640 	override int marginLeft() { return sw.marginLeft; }
5641 	override int marginRight() { return sw.marginRight; }
5642 	override int marginTop() { return sw.marginTop; }
5643 	override int marginBottom() { return sw.marginBottom; }
5644 	override int paddingLeft() { return sw.paddingLeft; }
5645 	override int paddingRight() { return sw.paddingRight; }
5646 	override int paddingTop() { return sw.paddingTop; }
5647 	override int paddingBottom() { return sw.paddingBottom; }
5648 	override void focus() { sw.focus(); }
5649 
5650 
5651 	override void recomputeChildLayout() {
5652 		// The stupid thing needs to calculate if a scroll bar is needed...
5653 		recomputeChildLayoutHelper();
5654 		// then running it again will position things correctly if the bar is NOT needed
5655 		recomputeChildLayoutHelper();
5656 
5657 		// this sucks but meh it barely works
5658 	}
5659 
5660 	private void recomputeChildLayoutHelper() {
5661 		if(sw is null) return;
5662 
5663 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
5664 		if(horizontalScrollBar && verticalScrollBar) {
5665 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
5666 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
5667 			horizontalScrollBar.x = 0;
5668 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
5669 
5670 			verticalScrollBar.width = verticalScrollBar.minWidth();
5671 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
5672 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
5673 			verticalScrollBar.y = 0 + 2;
5674 
5675 			sw.x = 0;
5676 			sw.y = 0;
5677 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
5678 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
5679 
5680 			if(sw.contentWidth_ <= this.width)
5681 				sw.scrollOrigin_.x = 0;
5682 			if(sw.contentHeight_ <= this.height)
5683 				sw.scrollOrigin_.y = 0;
5684 
5685 			horizontalScrollBar.recomputeChildLayout();
5686 			verticalScrollBar.recomputeChildLayout();
5687 			sw.recomputeChildLayout();
5688 		}
5689 
5690 		if(sw.contentWidth_ <= this.width)
5691 			sw.scrollOrigin_.x = 0;
5692 		if(sw.contentHeight_ <= this.height)
5693 			sw.scrollOrigin_.y = 0;
5694 
5695 		if(sw.showingHorizontalScroll())
5696 			horizontalScrollBar.showing(true, false);
5697 		else
5698 			horizontalScrollBar.showing(false, false);
5699 		if(sw.showingVerticalScroll())
5700 			verticalScrollBar.showing(true, false);
5701 		else
5702 			verticalScrollBar.showing(false, false);
5703 
5704 		verticalScrollBar.setViewableArea(sw.viewportHeight());
5705 		verticalScrollBar.setMax(sw.contentHeight);
5706 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
5707 
5708 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
5709 		horizontalScrollBar.setMax(sw.contentWidth);
5710 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
5711 	}
5712 }
5713 
5714 /*
5715 class ScrollableClientWidget : Widget {
5716 	this(Widget parent) {
5717 		super(parent);
5718 	}
5719 	override void paint(WidgetPainter p) {
5720 		parent.paint(p);
5721 	}
5722 }
5723 */
5724 
5725 /++
5726 	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.
5727 +/
5728 abstract class Slider : Widget {
5729 	this(int min, int max, int step, Widget parent) {
5730 		min_ = min;
5731 		max_ = max;
5732 		step_ = step;
5733 		page_ = step;
5734 		super(parent);
5735 	}
5736 
5737 	private int min_;
5738 	private int max_;
5739 	private int step_;
5740 	private int position_;
5741 	private int page_;
5742 
5743 	// selection start and selection end
5744 	// tics
5745 	// tooltip?
5746 	// some way to see and just type the value
5747 	// win32 buddy controls are labels
5748 
5749 	///
5750 	void setMin(int a) {
5751 		min_ = a;
5752 		version(custom_widgets)
5753 			redraw();
5754 		version(win32_widgets)
5755 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
5756 	}
5757 	///
5758 	int min() {
5759 		return min_;
5760 	}
5761 	///
5762 	void setMax(int a) {
5763 		max_ = a;
5764 		version(custom_widgets)
5765 			redraw();
5766 		version(win32_widgets)
5767 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
5768 	}
5769 	///
5770 	int max() {
5771 		return max_;
5772 	}
5773 	///
5774 	void setPosition(int a) {
5775 		if(a > max)
5776 			a = max;
5777 		if(a < min)
5778 			a = min;
5779 		position_ = a;
5780 		version(custom_widgets)
5781 			setPositionCustom(a);
5782 
5783 		version(win32_widgets)
5784 			setPositionWindows(a);
5785 	}
5786 	version(win32_widgets) {
5787 		protected abstract void setPositionWindows(int a);
5788 	}
5789 
5790 	protected abstract int win32direction();
5791 
5792 	/++
5793 		Alias for [position] for better compatibility with generic code.
5794 
5795 		History:
5796 			Added October 5, 2021
5797 	+/
5798 	@property int value() {
5799 		return position;
5800 	}
5801 
5802 	///
5803 	int position() {
5804 		return position_;
5805 	}
5806 	///
5807 	void setStep(int a) {
5808 		step_ = a;
5809 		version(win32_widgets)
5810 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
5811 	}
5812 	///
5813 	int step() {
5814 		return step_;
5815 	}
5816 	///
5817 	void setPageSize(int a) {
5818 		page_ = a;
5819 		version(win32_widgets)
5820 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
5821 	}
5822 	///
5823 	int pageSize() {
5824 		return page_;
5825 	}
5826 
5827 	private void notify() {
5828 		auto event = new ChangeEvent!int(this, &this.position);
5829 		event.dispatch();
5830 	}
5831 
5832 	version(win32_widgets)
5833 	void win32Setup(int style) {
5834 		createWin32Window(this, TRACKBAR_CLASS, "",
5835 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
5836 
5837 		// the trackbar sends the same messages as scroll, which
5838 		// our other layer sends as these... just gonna translate
5839 		// here
5840 		this.addDirectEventListener("scrolltoposition", (Event event) {
5841 			event.stopPropagation();
5842 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
5843 			notify();
5844 		});
5845 		this.addDirectEventListener("scrolltonextline", (Event event) {
5846 			event.stopPropagation();
5847 			this.setPosition(this.position + this.step_ * this.win32direction);
5848 			notify();
5849 		});
5850 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
5851 			event.stopPropagation();
5852 			this.setPosition(this.position - this.step_ * this.win32direction);
5853 			notify();
5854 		});
5855 		this.addDirectEventListener("scrolltonextpage", (Event event) {
5856 			event.stopPropagation();
5857 			this.setPosition(this.position + this.page_ * this.win32direction);
5858 			notify();
5859 		});
5860 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
5861 			event.stopPropagation();
5862 			this.setPosition(this.position - this.page_ * this.win32direction);
5863 			notify();
5864 		});
5865 
5866 		setMin(min_);
5867 		setMax(max_);
5868 		setStep(step_);
5869 		setPageSize(page_);
5870 	}
5871 
5872 	version(custom_widgets) {
5873 		protected MouseTrackingWidget thumb;
5874 
5875 		protected abstract void setPositionCustom(int a);
5876 
5877 		override void defaultEventHandler_keydown(KeyDownEvent event) {
5878 			switch(event.key) {
5879 				case Key.Up:
5880 				case Key.Right:
5881 					setPosition(position() - step() * win32direction);
5882 					changed();
5883 				break;
5884 				case Key.Down:
5885 				case Key.Left:
5886 					setPosition(position() + step() * win32direction);
5887 					changed();
5888 				break;
5889 				case Key.Home:
5890 					setPosition(win32direction > 0 ? min() : max());
5891 					changed();
5892 				break;
5893 				case Key.End:
5894 					setPosition(win32direction > 0 ? max() : min());
5895 					changed();
5896 				break;
5897 				case Key.PageUp:
5898 					setPosition(position() - pageSize() * win32direction);
5899 					changed();
5900 				break;
5901 				case Key.PageDown:
5902 					setPosition(position() + pageSize() * win32direction);
5903 					changed();
5904 				break;
5905 				default:
5906 			}
5907 			super.defaultEventHandler_keydown(event);
5908 		}
5909 
5910 		protected void changed() {
5911 			auto ev = new ChangeEvent!int(this, &position);
5912 			ev.dispatch();
5913 		}
5914 	}
5915 }
5916 
5917 /++
5918 
5919 +/
5920 class VerticalSlider : Slider {
5921 	this(int min, int max, int step, Widget parent) {
5922 		version(custom_widgets)
5923 			initialize();
5924 
5925 		super(min, max, step, parent);
5926 
5927 		version(win32_widgets)
5928 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
5929 	}
5930 
5931 	protected override int win32direction() {
5932 		return -1;
5933 	}
5934 
5935 	version(win32_widgets)
5936 	protected override void setPositionWindows(int a) {
5937 		// the windows thing makes the top 0 and i don't like that.
5938 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
5939 	}
5940 
5941 	version(custom_widgets)
5942 	private void initialize() {
5943 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
5944 
5945 		thumb.tabStop = false;
5946 
5947 		thumb.thumbWidth = width;
5948 		thumb.thumbHeight = scaleWithDpi(16);
5949 
5950 		thumb.addEventListener(EventType.change, () {
5951 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
5952 			sx = max - sx;
5953 			//informProgramThatUserChangedPosition(sx);
5954 
5955 			position_ = sx;
5956 
5957 			changed();
5958 		});
5959 	}
5960 
5961 	version(custom_widgets)
5962 	override void recomputeChildLayout() {
5963 		thumb.thumbWidth = this.width;
5964 		super.recomputeChildLayout();
5965 		setPositionCustom(position_);
5966 	}
5967 
5968 	version(custom_widgets)
5969 	protected override void setPositionCustom(int a) {
5970 		if(max())
5971 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
5972 		redraw();
5973 	}
5974 }
5975 
5976 /++
5977 
5978 +/
5979 class HorizontalSlider : Slider {
5980 	this(int min, int max, int step, Widget parent) {
5981 		version(custom_widgets)
5982 			initialize();
5983 
5984 		super(min, max, step, parent);
5985 
5986 		version(win32_widgets)
5987 			win32Setup(TBS_HORZ);
5988 	}
5989 
5990 	version(win32_widgets)
5991 	protected override void setPositionWindows(int a) {
5992 		SendMessage(hwnd, TBM_SETPOS, true, a);
5993 	}
5994 
5995 	protected override int win32direction() {
5996 		return 1;
5997 	}
5998 
5999 	version(custom_widgets)
6000 	private void initialize() {
6001 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6002 
6003 		thumb.tabStop = false;
6004 
6005 		thumb.thumbWidth = scaleWithDpi(16);
6006 		thumb.thumbHeight = height;
6007 
6008 		thumb.addEventListener(EventType.change, () {
6009 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6010 			//informProgramThatUserChangedPosition(sx);
6011 
6012 			position_ = sx;
6013 
6014 			changed();
6015 		});
6016 	}
6017 
6018 	version(custom_widgets)
6019 	override void recomputeChildLayout() {
6020 		thumb.thumbHeight = this.height;
6021 		super.recomputeChildLayout();
6022 		setPositionCustom(position_);
6023 	}
6024 
6025 	version(custom_widgets)
6026 	protected override void setPositionCustom(int a) {
6027 		if(max())
6028 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6029 		redraw();
6030 	}
6031 }
6032 
6033 
6034 ///
6035 abstract class ScrollbarBase : Widget {
6036 	///
6037 	this(Widget parent) {
6038 		super(parent);
6039 		tabStop = false;
6040 		step_ = scaleWithDpi(16);
6041 	}
6042 
6043 	private int viewableArea_;
6044 	private int max_;
6045 	private int step_;// = 16;
6046 	private int position_;
6047 
6048 	///
6049 	bool atEnd() {
6050 		return position_ + viewableArea_ >= max_;
6051 	}
6052 
6053 	///
6054 	bool atStart() {
6055 		return position_ == 0;
6056 	}
6057 
6058 	///
6059 	void setViewableArea(int a) {
6060 		viewableArea_ = a;
6061 		version(custom_widgets)
6062 			redraw();
6063 	}
6064 	///
6065 	void setMax(int a) {
6066 		max_ = a;
6067 		version(custom_widgets)
6068 			redraw();
6069 	}
6070 	///
6071 	int max() {
6072 		return max_;
6073 	}
6074 	///
6075 	void setPosition(int a) {
6076 		auto logicalMax = max_ - viewableArea_;
6077 		if(a == int.max)
6078 			a = logicalMax;
6079 
6080 		if(a > logicalMax)
6081 			a = logicalMax;
6082 		if(a < 0)
6083 			a = 0;
6084 
6085 		position_ = a;
6086 
6087 		version(custom_widgets)
6088 			redraw();
6089 	}
6090 	///
6091 	int position() {
6092 		return position_;
6093 	}
6094 	///
6095 	void setStep(int a) {
6096 		step_ = a;
6097 	}
6098 	///
6099 	int step() {
6100 		return step_;
6101 	}
6102 
6103 	// FIXME: remove this.... maybe
6104 	/+
6105 	protected void informProgramThatUserChangedPosition(int n) {
6106 		position_ = n;
6107 		auto evt = new Event(EventType.change, this);
6108 		evt.intValue = n;
6109 		evt.dispatch();
6110 	}
6111 	+/
6112 
6113 	version(custom_widgets) {
6114 		enum MIN_THUMB_SIZE = 8;
6115 
6116 		abstract protected int getBarDim();
6117 		int thumbSize() {
6118 			if(viewableArea_ >= max_ || max_ == 0)
6119 				return getBarDim();
6120 
6121 			int res = viewableArea_ * getBarDim() / max_;
6122 
6123 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6124 				res = scaleWithDpi(MIN_THUMB_SIZE);
6125 
6126 			return res;
6127 		}
6128 
6129 		int thumbPosition() {
6130 			/*
6131 				viewableArea_ is the viewport height/width
6132 				position_ is where we are
6133 			*/
6134 			//if(position_ + viewableArea_ >= max_)
6135 				//return getBarDim - thumbSize;
6136 
6137 			auto maximumPossibleValue = getBarDim() - thumbSize;
6138 			auto maximiumLogicalValue = max_ - viewableArea_;
6139 
6140 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6141 
6142 			return p;
6143 		}
6144 	}
6145 }
6146 
6147 //public import mgt;
6148 
6149 /++
6150 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6151 
6152 	Concrete subclasses may include a scrollbar thumb and a volume control.
6153 +/
6154 //version(custom_widgets)
6155 class MouseTrackingWidget : Widget {
6156 
6157 	///
6158 	int positionX() { return positionX_; }
6159 	///
6160 	int positionY() { return positionY_; }
6161 
6162 	///
6163 	void positionX(int p) { positionX_ = p; }
6164 	///
6165 	void positionY(int p) { positionY_ = p; }
6166 
6167 	private int positionX_;
6168 	private int positionY_;
6169 
6170 	///
6171 	enum Orientation {
6172 		horizontal, ///
6173 		vertical, ///
6174 		twoDimensional, ///
6175 	}
6176 
6177 	private int thumbWidth_;
6178 	private int thumbHeight_;
6179 
6180 	///
6181 	int thumbWidth() { return thumbWidth_; }
6182 	///
6183 	int thumbHeight() { return thumbHeight_; }
6184 	///
6185 	int thumbWidth(int a) { return thumbWidth_ = a; }
6186 	///
6187 	int thumbHeight(int a) { return thumbHeight_ = a; }
6188 
6189 	private bool dragging;
6190 	private bool hovering;
6191 	private int startMouseX, startMouseY;
6192 
6193 	///
6194 	this(Orientation orientation, Widget parent) {
6195 		super(parent);
6196 
6197 		//assert(parentWindow !is null);
6198 
6199 		addEventListener((MouseDownEvent event) {
6200 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6201 				dragging = true;
6202 				startMouseX = event.clientX - positionX;
6203 				startMouseY = event.clientY - positionY;
6204 				parentWindow.captureMouse(this);
6205 			} else {
6206 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6207 					positionX = event.clientX - thumbWidth / 2;
6208 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6209 					positionY = event.clientY - thumbHeight / 2;
6210 
6211 				if(positionX + thumbWidth > this.width)
6212 					positionX = this.width - thumbWidth;
6213 				if(positionY + thumbHeight > this.height)
6214 					positionY = this.height - thumbHeight;
6215 
6216 				if(positionX < 0)
6217 					positionX = 0;
6218 				if(positionY < 0)
6219 					positionY = 0;
6220 
6221 
6222 				// this.emit!(ChangeEvent!void)();
6223 				auto evt = new Event(EventType.change, this);
6224 				evt.sendDirectly();
6225 
6226 				redraw();
6227 
6228 			}
6229 		});
6230 
6231 		addEventListener(EventType.mouseup, (Event event) {
6232 			dragging = false;
6233 			parentWindow.releaseMouseCapture();
6234 		});
6235 
6236 		addEventListener(EventType.mouseout, (Event event) {
6237 			if(!hovering)
6238 				return;
6239 			hovering = false;
6240 			redraw();
6241 		});
6242 
6243 		int lpx, lpy;
6244 
6245 		addEventListener((MouseMoveEvent event) {
6246 			auto oh = hovering;
6247 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6248 				hovering = true;
6249 			} else {
6250 				hovering = false;
6251 			}
6252 			if(!dragging) {
6253 				if(hovering != oh)
6254 					redraw();
6255 				return;
6256 			}
6257 
6258 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6259 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
6260 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6261 				positionY = event.clientY - startMouseY;
6262 
6263 			if(positionX + thumbWidth > this.width)
6264 				positionX = this.width - thumbWidth;
6265 			if(positionY + thumbHeight > this.height)
6266 				positionY = this.height - thumbHeight;
6267 
6268 			if(positionX < 0)
6269 				positionX = 0;
6270 			if(positionY < 0)
6271 				positionY = 0;
6272 
6273 			if(positionX != lpx || positionY != lpy) {
6274 				lpx = positionX;
6275 				lpy = positionY;
6276 
6277 				auto evt = new Event(EventType.change, this);
6278 				evt.sendDirectly();
6279 			}
6280 
6281 			redraw();
6282 		});
6283 	}
6284 
6285 	version(custom_widgets)
6286 	override void paint(WidgetPainter painter) {
6287 		auto cs = getComputedStyle();
6288 		auto c = darken(cs.windowBackgroundColor, 0.2);
6289 		painter.outlineColor = c;
6290 		painter.fillColor = c;
6291 		painter.drawRectangle(Point(0, 0), this.width, this.height);
6292 
6293 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
6294 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
6295 	}
6296 }
6297 
6298 //version(custom_widgets)
6299 //private
6300 class HorizontalScrollbar : ScrollbarBase {
6301 
6302 	version(custom_widgets) {
6303 		private MouseTrackingWidget thumb;
6304 
6305 		override int getBarDim() {
6306 			return thumb.width;
6307 		}
6308 	}
6309 
6310 	override void setViewableArea(int a) {
6311 		super.setViewableArea(a);
6312 
6313 		version(win32_widgets) {
6314 			SCROLLINFO info;
6315 			info.cbSize = info.sizeof;
6316 			info.nPage = a + 1;
6317 			info.fMask = SIF_PAGE;
6318 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6319 		} else version(custom_widgets) {
6320 			thumb.positionX = thumbPosition;
6321 			thumb.thumbWidth = thumbSize;
6322 			thumb.redraw();
6323 		} else static assert(0);
6324 
6325 	}
6326 
6327 	override void setMax(int a) {
6328 		super.setMax(a);
6329 		version(win32_widgets) {
6330 			SCROLLINFO info;
6331 			info.cbSize = info.sizeof;
6332 			info.nMin = 0;
6333 			info.nMax = max;
6334 			info.fMask = SIF_RANGE;
6335 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6336 		} else version(custom_widgets) {
6337 			thumb.positionX = thumbPosition;
6338 			thumb.thumbWidth = thumbSize;
6339 			thumb.redraw();
6340 		}
6341 	}
6342 
6343 	override void setPosition(int a) {
6344 		super.setPosition(a);
6345 		version(win32_widgets) {
6346 			SCROLLINFO info;
6347 			info.cbSize = info.sizeof;
6348 			info.fMask = SIF_POS;
6349 			info.nPos = position;
6350 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6351 		} else version(custom_widgets) {
6352 			thumb.positionX = thumbPosition();
6353 			thumb.thumbWidth = thumbSize;
6354 			thumb.redraw();
6355 		} else static assert(0);
6356 	}
6357 
6358 	this(Widget parent) {
6359 		super(parent);
6360 
6361 		version(win32_widgets) {
6362 			createWin32Window(this, "Scrollbar"w, "",
6363 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
6364 		} else version(custom_widgets) {
6365 			auto vl = new HorizontalLayout(this);
6366 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
6367 			leftButton.setClickRepeat(scrollClickRepeatInterval);
6368 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
6369 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
6370 			rightButton.setClickRepeat(scrollClickRepeatInterval);
6371 
6372 			leftButton.tabStop = false;
6373 			rightButton.tabStop = false;
6374 			thumb.tabStop = false;
6375 
6376 			leftButton.addEventListener(EventType.triggered, () {
6377 				this.emitCommand!"scrolltopreviousline"();
6378 				//informProgramThatUserChangedPosition(position - step());
6379 			});
6380 			rightButton.addEventListener(EventType.triggered, () {
6381 				this.emitCommand!"scrolltonextline"();
6382 				//informProgramThatUserChangedPosition(position + step());
6383 			});
6384 
6385 			thumb.thumbWidth = this.minWidth;
6386 			thumb.thumbHeight = scaleWithDpi(16);
6387 
6388 			thumb.addEventListener(EventType.change, () {
6389 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
6390 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
6391 
6392 				//informProgramThatUserChangedPosition(sx);
6393 
6394 				auto ev = new ScrollToPositionEvent(this, sx);
6395 				ev.dispatch();
6396 			});
6397 		}
6398 	}
6399 
6400 	override int minHeight() { return scaleWithDpi(16); }
6401 	override int maxHeight() { return scaleWithDpi(16); }
6402 	override int minWidth() { return scaleWithDpi(48); }
6403 }
6404 
6405 class ScrollToPositionEvent : Event {
6406 	enum EventString = "scrolltoposition";
6407 
6408 	this(Widget target, int value) {
6409 		this.value = value;
6410 		super(EventString, target);
6411 	}
6412 
6413 	immutable int value;
6414 
6415 	override @property int intValue() {
6416 		return value;
6417 	}
6418 }
6419 
6420 //version(custom_widgets)
6421 //private
6422 class VerticalScrollbar : ScrollbarBase {
6423 
6424 	version(custom_widgets) {
6425 		override int getBarDim() {
6426 			return thumb.height;
6427 		}
6428 
6429 		private MouseTrackingWidget thumb;
6430 	}
6431 
6432 	override void setViewableArea(int a) {
6433 		super.setViewableArea(a);
6434 
6435 		version(win32_widgets) {
6436 			SCROLLINFO info;
6437 			info.cbSize = info.sizeof;
6438 			info.nPage = a + 1;
6439 			info.fMask = SIF_PAGE;
6440 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6441 		} else version(custom_widgets) {
6442 			thumb.positionY = thumbPosition;
6443 			thumb.thumbHeight = thumbSize;
6444 			thumb.redraw();
6445 		} else static assert(0);
6446 
6447 	}
6448 
6449 	override void setMax(int a) {
6450 		super.setMax(a);
6451 		version(win32_widgets) {
6452 			SCROLLINFO info;
6453 			info.cbSize = info.sizeof;
6454 			info.nMin = 0;
6455 			info.nMax = max;
6456 			info.fMask = SIF_RANGE;
6457 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6458 		} else version(custom_widgets) {
6459 			thumb.positionY = thumbPosition;
6460 			thumb.thumbHeight = thumbSize;
6461 			thumb.redraw();
6462 		}
6463 	}
6464 
6465 	override void setPosition(int a) {
6466 		super.setPosition(a);
6467 		version(win32_widgets) {
6468 			SCROLLINFO info;
6469 			info.cbSize = info.sizeof;
6470 			info.fMask = SIF_POS;
6471 			info.nPos = position;
6472 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6473 		} else version(custom_widgets) {
6474 			thumb.positionY = thumbPosition;
6475 			thumb.thumbHeight = thumbSize;
6476 			thumb.redraw();
6477 		} else static assert(0);
6478 	}
6479 
6480 	this(Widget parent) {
6481 		super(parent);
6482 
6483 		version(win32_widgets) {
6484 			createWin32Window(this, "Scrollbar"w, "",
6485 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
6486 		} else version(custom_widgets) {
6487 			auto vl = new VerticalLayout(this);
6488 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
6489 			upButton.setClickRepeat(scrollClickRepeatInterval);
6490 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
6491 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
6492 			downButton.setClickRepeat(scrollClickRepeatInterval);
6493 
6494 			upButton.addEventListener(EventType.triggered, () {
6495 				this.emitCommand!"scrolltopreviousline"();
6496 				//informProgramThatUserChangedPosition(position - step());
6497 			});
6498 			downButton.addEventListener(EventType.triggered, () {
6499 				this.emitCommand!"scrolltonextline"();
6500 				//informProgramThatUserChangedPosition(position + step());
6501 			});
6502 
6503 			thumb.thumbWidth = this.minWidth;
6504 			thumb.thumbHeight = scaleWithDpi(16);
6505 
6506 			thumb.addEventListener(EventType.change, () {
6507 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
6508 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
6509 
6510 				auto ev = new ScrollToPositionEvent(this, sy);
6511 				ev.dispatch();
6512 
6513 				//informProgramThatUserChangedPosition(sy);
6514 			});
6515 
6516 			upButton.tabStop = false;
6517 			downButton.tabStop = false;
6518 			thumb.tabStop = false;
6519 		}
6520 	}
6521 
6522 	override int minWidth() { return scaleWithDpi(16); }
6523 	override int maxWidth() { return scaleWithDpi(16); }
6524 	override int minHeight() { return scaleWithDpi(48); }
6525 }
6526 
6527 
6528 /++
6529 	EXPERIMENTAL
6530 
6531 	A widget specialized for being a container for other widgets.
6532 
6533 	History:
6534 		Added May 29, 2021. Not stabilized at this time.
6535 +/
6536 class WidgetContainer : Widget {
6537 	this(Widget parent) {
6538 		tabStop = false;
6539 		super(parent);
6540 	}
6541 
6542 	override int maxHeight() {
6543 		if(this.children.length == 1) {
6544 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
6545 		} else {
6546 			return int.max;
6547 		}
6548 	}
6549 
6550 	override int maxWidth() {
6551 		if(this.children.length == 1) {
6552 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
6553 		} else {
6554 			return int.max;
6555 		}
6556 	}
6557 
6558 	/+
6559 
6560 	override int minHeight() {
6561 		int largest = 0;
6562 		int margins = 0;
6563 		int lastMargin = 0;
6564 		foreach(child; children) {
6565 			auto mh = child.minHeight();
6566 			if(mh > largest)
6567 				largest = mh;
6568 			margins += mymax(lastMargin, child.marginTop());
6569 			lastMargin = child.marginBottom();
6570 		}
6571 		return largest + margins;
6572 	}
6573 
6574 	override int maxHeight() {
6575 		int largest = 0;
6576 		int margins = 0;
6577 		int lastMargin = 0;
6578 		foreach(child; children) {
6579 			auto mh = child.maxHeight();
6580 			if(mh == int.max)
6581 				return int.max;
6582 			if(mh > largest)
6583 				largest = mh;
6584 			margins += mymax(lastMargin, child.marginTop());
6585 			lastMargin = child.marginBottom();
6586 		}
6587 		return largest + margins;
6588 	}
6589 
6590 	override int minWidth() {
6591 		int min;
6592 		foreach(child; children) {
6593 			auto cm = child.minWidth;
6594 			if(cm > min)
6595 				min = cm;
6596 		}
6597 		return min + paddingLeft + paddingRight;
6598 	}
6599 
6600 	override int minHeight() {
6601 		int min;
6602 		foreach(child; children) {
6603 			auto cm = child.minHeight;
6604 			if(cm > min)
6605 				min = cm;
6606 		}
6607 		return min + paddingTop + paddingBottom;
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 heightStretchiness() {
6627 		int max;
6628 		foreach(child; children) {
6629 			auto c = child.heightStretchiness;
6630 			if(c > max)
6631 				max = c;
6632 		}
6633 		return max;
6634 	}
6635 
6636 	override int marginTop() {
6637 		if(this.children.length)
6638 			return this.children[0].marginTop;
6639 		return 0;
6640 	}
6641 	+/
6642 }
6643 
6644 ///
6645 abstract class Layout : Widget {
6646 	this(Widget parent) {
6647 		tabStop = false;
6648 		super(parent);
6649 	}
6650 }
6651 
6652 /++
6653 	Makes all children minimum width and height, placing them down
6654 	left to right, top to bottom.
6655 
6656 	Useful if you want to make a list of buttons that automatically
6657 	wrap to a new line when necessary.
6658 +/
6659 class InlineBlockLayout : Layout {
6660 	///
6661 	this(Widget parent) { super(parent); }
6662 
6663 	override void recomputeChildLayout() {
6664 		registerMovement();
6665 
6666 		int x = this.paddingLeft, y = this.paddingTop;
6667 
6668 		int lineHeight;
6669 		int previousMargin = 0;
6670 		int previousMarginBottom = 0;
6671 
6672 		foreach(child; children) {
6673 			if(child.hidden)
6674 				continue;
6675 			if(cast(FixedPosition) child) {
6676 				child.recomputeChildLayout();
6677 				continue;
6678 			}
6679 			child.width = child.flexBasisWidth();
6680 			if(child.width == 0)
6681 				child.width = child.minWidth();
6682 			if(child.width == 0)
6683 				child.width = 32;
6684 
6685 			child.height = child.flexBasisHeight();
6686 			if(child.height == 0)
6687 				child.height = child.minHeight();
6688 			if(child.height == 0)
6689 				child.height = 32;
6690 
6691 			if(x + child.width + paddingRight > this.width) {
6692 				x = this.paddingLeft;
6693 				y += lineHeight;
6694 				lineHeight = 0;
6695 				previousMargin = 0;
6696 				previousMarginBottom = 0;
6697 			}
6698 
6699 			auto margin = child.marginLeft;
6700 			if(previousMargin > margin)
6701 				margin = previousMargin;
6702 
6703 			x += margin;
6704 
6705 			child.x = x;
6706 			child.y = y;
6707 
6708 			int marginTopApplied;
6709 			if(child.marginTop > previousMarginBottom) {
6710 				child.y += child.marginTop;
6711 				marginTopApplied = child.marginTop;
6712 			}
6713 
6714 			x += child.width;
6715 			previousMargin = child.marginRight;
6716 
6717 			if(child.marginBottom > previousMarginBottom)
6718 				previousMarginBottom = child.marginBottom;
6719 
6720 			auto h = child.height + previousMarginBottom + marginTopApplied;
6721 			if(h > lineHeight)
6722 				lineHeight = h;
6723 
6724 			child.recomputeChildLayout();
6725 		}
6726 
6727 	}
6728 
6729 	override int minWidth() {
6730 		int min;
6731 		foreach(child; children) {
6732 			auto cm = child.minWidth;
6733 			if(cm > min)
6734 				min = cm;
6735 		}
6736 		return min + paddingLeft + paddingRight;
6737 	}
6738 
6739 	override int minHeight() {
6740 		int min;
6741 		foreach(child; children) {
6742 			auto cm = child.minHeight;
6743 			if(cm > min)
6744 				min = cm;
6745 		}
6746 		return min + paddingTop + paddingBottom;
6747 	}
6748 }
6749 
6750 /++
6751 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
6752 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
6753 	the [TabWidget] will automatically change pages of child widgets.
6754 
6755 	This allows you to react to it however you see fit rather than having to
6756 	be tied to just the new sets of child widgets.
6757 
6758 	It sends the message in the form of `this.emitCommand!"changetab"();`.
6759 
6760 	History:
6761 		Added December 24, 2021 (dub v10.5)
6762 +/
6763 class TabMessageWidget : Widget {
6764 
6765 	protected void tabIndexClicked(int item) {
6766 		this.emitCommand!"changetab"();
6767 	}
6768 
6769 	/++
6770 		Adds the a new tab to the control with the given title.
6771 
6772 		Returns:
6773 			The index of the newly added tab. You will need to know
6774 			this index to refer to it later and to know which tab to
6775 			change to when you get a changetab message.
6776 	+/
6777 	int addTab(string title, int pos = int.max) {
6778 		version(win32_widgets) {
6779 			TCITEM item;
6780 			item.mask = TCIF_TEXT;
6781 			WCharzBuffer buf = WCharzBuffer(title);
6782 			item.pszText = buf.ptr;
6783 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
6784 		} else version(custom_widgets) {
6785 			if(pos >= tabs.length) {
6786 				tabs ~= title;
6787 				redraw();
6788 				return cast(int) tabs.length - 1;
6789 			} else if(pos <= 0) {
6790 				tabs = title ~ tabs;
6791 				redraw();
6792 				return 0;
6793 			} else {
6794 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
6795 				redraw();
6796 				return pos;
6797 			}
6798 		}
6799 	}
6800 
6801 	override void addChild(Widget child, int pos = int.max) {
6802 		if(container)
6803 			container.addChild(child, pos);
6804 		else
6805 			super.addChild(child, pos);
6806 	}
6807 
6808 	protected Widget makeContainer() {
6809 		return new Widget(this);
6810 	}
6811 
6812 	private Widget container;
6813 
6814 	override void recomputeChildLayout() {
6815 		version(win32_widgets) {
6816 			this.registerMovement();
6817 
6818 			RECT rect;
6819 			GetWindowRect(hwnd, &rect);
6820 
6821 			auto left = rect.left;
6822 			auto top = rect.top;
6823 
6824 			TabCtrl_AdjustRect(hwnd, false, &rect);
6825 			foreach(child; children) {
6826 				if(!child.showing) continue;
6827 				child.x = rect.left - left;
6828 				child.y = rect.top - top;
6829 				child.width = rect.right - rect.left;
6830 				child.height = rect.bottom - rect.top;
6831 				child.recomputeChildLayout();
6832 			}
6833 		} else version(custom_widgets) {
6834 			this.registerMovement();
6835 			foreach(child; children) {
6836 				if(!child.showing) continue;
6837 				child.x = 2;
6838 				child.y = tabBarHeight + 2; // for the border
6839 				child.width = width - 4; // for the border
6840 				child.height = height - tabBarHeight - 2 - 2; // for the border
6841 				child.recomputeChildLayout();
6842 			}
6843 		} else static assert(0);
6844 	}
6845 
6846 	version(custom_widgets)
6847 		string[] tabs;
6848 
6849 	this(Widget parent) {
6850 		super(parent);
6851 
6852 		tabStop = false;
6853 
6854 		version(win32_widgets) {
6855 			createWin32Window(this, WC_TABCONTROL, "", 0);
6856 		} else version(custom_widgets) {
6857 			addEventListener((ClickEvent event) {
6858 				if(event.target !is this)
6859 					return;
6860 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
6861 					auto t = (event.clientX / tabWidth);
6862 					if(t >= 0 && t < tabs.length) {
6863 						currentTab_ = t;
6864 						tabIndexClicked(t);
6865 						redraw();
6866 					}
6867 				}
6868 			});
6869 		} else static assert(0);
6870 
6871 		this.container = makeContainer();
6872 	}
6873 
6874 	override int marginTop() { return 4; }
6875 	override int paddingBottom() { return 4; }
6876 
6877 	override int minHeight() {
6878 		int max = 0;
6879 		foreach(child; children)
6880 			max = mymax(child.minHeight, max);
6881 
6882 
6883 		version(win32_widgets) {
6884 			RECT rect;
6885 			rect.right = this.width;
6886 			rect.bottom = max;
6887 			TabCtrl_AdjustRect(hwnd, true, &rect);
6888 
6889 			max = rect.bottom;
6890 		} else {
6891 			max += defaultLineHeight + 4;
6892 		}
6893 
6894 
6895 		return max;
6896 	}
6897 
6898 	version(win32_widgets)
6899 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
6900 		switch(code) {
6901 			case TCN_SELCHANGE:
6902 				auto sel = TabCtrl_GetCurSel(hwnd);
6903 				tabIndexClicked(sel);
6904 			break;
6905 			default:
6906 		}
6907 		return 0;
6908 	}
6909 
6910 	version(custom_widgets) {
6911 		private int currentTab_;
6912 		private int tabBarHeight() { return defaultLineHeight; }
6913 		int tabWidth() { return scaleWithDpi(80); }
6914 	}
6915 
6916 	version(win32_widgets)
6917 	override void paint(WidgetPainter painter) {}
6918 
6919 	version(custom_widgets)
6920 	override void paint(WidgetPainter painter) {
6921 		auto cs = getComputedStyle();
6922 
6923 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
6924 
6925 		int posX = 0;
6926 		foreach(idx, title; tabs) {
6927 			auto isCurrent = idx == getCurrentTab();
6928 
6929 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
6930 
6931 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
6932 			painter.outlineColor = cs.foregroundColor;
6933 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
6934 
6935 			if(isCurrent) {
6936 				painter.outlineColor = cs.windowBackgroundColor;
6937 				painter.fillColor = Color.transparent;
6938 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
6939 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
6940 
6941 				painter.outlineColor = Color.white;
6942 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
6943 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
6944 				painter.outlineColor = cs.activeTabColor;
6945 				painter.drawPixel(Point(posX, tabBarHeight - 1));
6946 			}
6947 
6948 			posX += tabWidth - 2;
6949 		}
6950 	}
6951 
6952 	///
6953 	@scriptable
6954 	void setCurrentTab(int item) {
6955 		version(win32_widgets)
6956 			TabCtrl_SetCurSel(hwnd, item);
6957 		else version(custom_widgets)
6958 			currentTab_ = item;
6959 		else static assert(0);
6960 
6961 		tabIndexClicked(item);
6962 	}
6963 
6964 	///
6965 	@scriptable
6966 	int getCurrentTab() {
6967 		version(win32_widgets)
6968 			return TabCtrl_GetCurSel(hwnd);
6969 		else version(custom_widgets)
6970 			return currentTab_; // FIXME
6971 		else static assert(0);
6972 	}
6973 
6974 	///
6975 	@scriptable
6976 	void removeTab(int item) {
6977 		if(item && item == getCurrentTab())
6978 			setCurrentTab(item - 1);
6979 
6980 		version(win32_widgets) {
6981 			TabCtrl_DeleteItem(hwnd, item);
6982 		}
6983 
6984 		for(int a = item; a < children.length - 1; a++)
6985 			this._children[a] = this._children[a + 1];
6986 		this._children = this._children[0 .. $-1];
6987 	}
6988 
6989 }
6990 
6991 
6992 /++
6993 	A tab widget is a set of clickable tab buttons followed by a content area.
6994 
6995 
6996 	Tabs can change existing content or can be new pages.
6997 
6998 	When the user picks a different tab, a `change` message is generated.
6999 +/
7000 class TabWidget : TabMessageWidget {
7001 	this(Widget parent) {
7002 		super(parent);
7003 	}
7004 
7005 	override protected Widget makeContainer() {
7006 		return null;
7007 	}
7008 
7009 	override void addChild(Widget child, int pos = int.max) {
7010 		if(auto twp = cast(TabWidgetPage) child) {
7011 			Widget.addChild(child, pos);
7012 			if(pos == int.max)
7013 				pos = cast(int) this.children.length - 1;
7014 
7015 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7016 
7017 			if(pos != getCurrentTab) {
7018 				child.showing = false;
7019 			}
7020 		} else {
7021 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7022 		}
7023 	}
7024 
7025 	// FIXME: add tab icons at some point, Windows supports them
7026 	/++
7027 		Adds a page and its associated tab with the given label to the widget.
7028 
7029 		Returns:
7030 			The added page object, to which you can add other widgets.
7031 	+/
7032 	@scriptable
7033 	TabWidgetPage addPage(string title) {
7034 		return new TabWidgetPage(title, this);
7035 	}
7036 
7037 	/++
7038 		Gets the page at the given tab index, or `null` if the index is bad.
7039 
7040 		History:
7041 			Added December 24, 2021.
7042 	+/
7043 	TabWidgetPage getPage(int index) {
7044 		if(index < this.children.length)
7045 			return null;
7046 		return cast(TabWidgetPage) this.children[index];
7047 	}
7048 
7049 	/++
7050 		While you can still use the addTab from the parent class,
7051 		*strongly* recommend you use [addPage] insteaad.
7052 
7053 		History:
7054 			Added December 24, 2021 to fulful the interface
7055 			requirement that came from adding [TabMessageWidget].
7056 
7057 			You should not use it though since the [addPage] function
7058 			is much easier to use here.
7059 	+/
7060 	override int addTab(string title, int pos = int.max) {
7061 		auto p = addPage(title);
7062 		foreach(idx, child; this.children)
7063 			if(child is p)
7064 				return cast(int) idx;
7065 		return -1;
7066 	}
7067 
7068 	protected override void tabIndexClicked(int item) {
7069 		foreach(idx, child; children) {
7070 			child.showing(false, false); // batch the recalculates for the end
7071 		}
7072 
7073 		foreach(idx, child; children) {
7074 			if(idx == item) {
7075 				child.showing(true, false);
7076 				if(parentWindow) {
7077 					auto f = parentWindow.getFirstFocusable(child);
7078 					if(f)
7079 						f.focus();
7080 				}
7081 				recomputeChildLayout();
7082 			}
7083 		}
7084 
7085 		version(win32_widgets) {
7086 			InvalidateRect(hwnd, null, true);
7087 		} else version(custom_widgets) {
7088 			this.redraw();
7089 		}
7090 	}
7091 
7092 }
7093 
7094 /++
7095 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7096 
7097 	You add [TabWidgetPage]s to it.
7098 +/
7099 class PageWidget : Widget {
7100 	this(Widget parent) {
7101 		super(parent);
7102 	}
7103 
7104 	override int minHeight() {
7105 		int max = 0;
7106 		foreach(child; children)
7107 			max = mymax(child.minHeight, max);
7108 
7109 		return max;
7110 	}
7111 
7112 
7113 	override void addChild(Widget child, int pos = int.max) {
7114 		if(auto twp = cast(TabWidgetPage) child) {
7115 			super.addChild(child, pos);
7116 			if(pos == int.max)
7117 				pos = cast(int) this.children.length - 1;
7118 
7119 			if(pos != getCurrentTab) {
7120 				child.showing = false;
7121 			}
7122 		} else {
7123 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7124 		}
7125 	}
7126 
7127 	override void recomputeChildLayout() {
7128 		this.registerMovement();
7129 		foreach(child; children) {
7130 			child.x = 0;
7131 			child.y = 0;
7132 			child.width = width;
7133 			child.height = height;
7134 			child.recomputeChildLayout();
7135 		}
7136 	}
7137 
7138 	private int currentTab_;
7139 
7140 	///
7141 	@scriptable
7142 	void setCurrentTab(int item) {
7143 		currentTab_ = item;
7144 
7145 		showOnly(item);
7146 	}
7147 
7148 	///
7149 	@scriptable
7150 	int getCurrentTab() {
7151 		return currentTab_;
7152 	}
7153 
7154 	///
7155 	@scriptable
7156 	void removeTab(int item) {
7157 		if(item && item == getCurrentTab())
7158 			setCurrentTab(item - 1);
7159 
7160 		for(int a = item; a < children.length - 1; a++)
7161 			this._children[a] = this._children[a + 1];
7162 		this._children = this._children[0 .. $-1];
7163 	}
7164 
7165 	///
7166 	@scriptable
7167 	TabWidgetPage addPage(string title) {
7168 		return new TabWidgetPage(title, this);
7169 	}
7170 
7171 	private void showOnly(int item) {
7172 		foreach(idx, child; children)
7173 			if(idx == item) {
7174 				child.show();
7175 				child.recomputeChildLayout();
7176 			} else {
7177 				child.hide();
7178 			}
7179 	}
7180 
7181 }
7182 
7183 /++
7184 
7185 +/
7186 class TabWidgetPage : Widget {
7187 	string title;
7188 	this(string title, Widget parent) {
7189 		this.title = title;
7190 		this.tabStop = false;
7191 		super(parent);
7192 
7193 		///*
7194 		version(win32_widgets) {
7195 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7196 		}
7197 		//*/
7198 	}
7199 
7200 	override int minHeight() {
7201 		int sum = 0;
7202 		foreach(child; children)
7203 			sum += child.minHeight();
7204 		return sum;
7205 	}
7206 }
7207 
7208 version(none)
7209 /++
7210 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7211 
7212 	I think I need to modify the layout algorithms to support this.
7213 +/
7214 class CollapsableSidebar : Widget {
7215 
7216 }
7217 
7218 /// Stacks the widgets vertically, taking all the available width for each child.
7219 class VerticalLayout : Layout {
7220 	// most of this is intentionally blank - widget's default is vertical layout right now
7221 	///
7222 	this(Widget parent) { super(parent); }
7223 
7224 	/++
7225 		Sets a max width for the layout so you don't have to subclass. The max width
7226 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7227 
7228 		History:
7229 			Added November 29, 2021 (dub v10.5)
7230 	+/
7231 	this(int maxWidth, Widget parent) {
7232 		this.mw = maxWidth;
7233 		super(parent);
7234 	}
7235 
7236 	private int mw = int.max;
7237 
7238 	override int maxWidth() { return scaleWithDpi(mw); }
7239 }
7240 
7241 /// Stacks the widgets horizontally, taking all the available height for each child.
7242 class HorizontalLayout : Layout {
7243 	///
7244 	this(Widget parent) { super(parent); }
7245 
7246 	/++
7247 		Sets a max height for the layout so you don't have to subclass. The max height
7248 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7249 
7250 		History:
7251 			Added November 29, 2021 (dub v10.5)
7252 	+/
7253 	this(int maxHeight, Widget parent) {
7254 		this.mh = maxHeight;
7255 		super(parent);
7256 	}
7257 
7258 	private int mh = 0;
7259 
7260 
7261 
7262 	override void recomputeChildLayout() {
7263 		.recomputeChildLayout!"width"(this);
7264 	}
7265 
7266 	override int minHeight() {
7267 		int largest = 0;
7268 		int margins = 0;
7269 		int lastMargin = 0;
7270 		foreach(child; children) {
7271 			auto mh = child.minHeight();
7272 			if(mh > largest)
7273 				largest = mh;
7274 			margins += mymax(lastMargin, child.marginTop());
7275 			lastMargin = child.marginBottom();
7276 		}
7277 		return largest + margins;
7278 	}
7279 
7280 	override int maxHeight() {
7281 		if(mh != 0)
7282 			return mymax(minHeight, scaleWithDpi(mh));
7283 
7284 		int largest = 0;
7285 		int margins = 0;
7286 		int lastMargin = 0;
7287 		foreach(child; children) {
7288 			auto mh = child.maxHeight();
7289 			if(mh == int.max)
7290 				return int.max;
7291 			if(mh > largest)
7292 				largest = mh;
7293 			margins += mymax(lastMargin, child.marginTop());
7294 			lastMargin = child.marginBottom();
7295 		}
7296 		return largest + margins;
7297 	}
7298 
7299 	override int heightStretchiness() {
7300 		int max;
7301 		foreach(child; children) {
7302 			auto c = child.heightStretchiness;
7303 			if(c > max)
7304 				max = c;
7305 		}
7306 		return max;
7307 	}
7308 
7309 }
7310 
7311 version(win32_widgets)
7312 private
7313 extern(Windows)
7314 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
7315 	Widget* pwin = hwnd in Widget.nativeMapping;
7316 	if(pwin is null)
7317 		return DefWindowProc(hwnd, message, wparam, lparam);
7318 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
7319 	if(win is null)
7320 		return DefWindowProc(hwnd, message, wparam, lparam);
7321 
7322 	switch(message) {
7323 		case WM_SIZE:
7324 			auto width = LOWORD(lparam);
7325 			auto height = HIWORD(lparam);
7326 
7327 			auto hdc = GetDC(hwnd);
7328 			auto hdcBmp = CreateCompatibleDC(hdc);
7329 
7330 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
7331 			if(width > win.bmpWidth || height > win.bmpHeight) {
7332 				auto oldBuffer = win.buffer;
7333 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
7334 
7335 				if(oldBuffer)
7336 					DeleteObject(oldBuffer);
7337 
7338 				win.bmpWidth = width;
7339 				win.bmpHeight = height;
7340 			}
7341 
7342 			// just always erase it upon resizing so minigui can draw over with a clean slate
7343 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
7344 
7345 			auto brush = GetSysColorBrush(COLOR_3DFACE);
7346 			RECT r;
7347 			r.left = 0;
7348 			r.top = 0;
7349 			r.right = width;
7350 			r.bottom = height;
7351 			FillRect(hdcBmp, &r, brush);
7352 
7353 			SelectObject(hdcBmp, oldBmp);
7354 			DeleteDC(hdcBmp);
7355 			ReleaseDC(hwnd, hdc);
7356 		break;
7357 		case WM_PAINT:
7358 			if(win.buffer is null)
7359 				goto default;
7360 
7361 			BITMAP bm;
7362 			PAINTSTRUCT ps;
7363 
7364 			HDC hdc = BeginPaint(hwnd, &ps);
7365 
7366 			HDC hdcMem = CreateCompatibleDC(hdc);
7367 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
7368 
7369 			GetObject(win.buffer, bm.sizeof, &bm);
7370 
7371 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
7372 
7373 			SelectObject(hdcMem, hbmOld);
7374 			DeleteDC(hdcMem);
7375 			EndPaint(hwnd, &ps);
7376 		break;
7377 		default:
7378 			return DefWindowProc(hwnd, message, wparam, lparam);
7379 	}
7380 
7381 	return 0;
7382 }
7383 
7384 private wstring Win32Class(wstring name)() {
7385 	static bool classRegistered;
7386 	if(!classRegistered) {
7387 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
7388 		WNDCLASSEX wc;
7389 		wc.cbSize = wc.sizeof;
7390 		wc.hInstance = hInstance;
7391 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
7392 		wc.lpfnWndProc = &DoubleBufferWndProc;
7393 		wc.lpszClassName = name.ptr;
7394 		if(!RegisterClassExW(&wc))
7395 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
7396 		classRegistered = true;
7397 	}
7398 
7399 		return name;
7400 }
7401 
7402 /+
7403 version(win32_widgets)
7404 extern(Windows)
7405 private
7406 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
7407 	switch(iMessage) {
7408 		case WM_PAINT:
7409 			if(auto te = hWnd in Widget.nativeMapping) {
7410 				try {
7411 					//te.redraw();
7412 					writeln(te, " drawing");
7413 				} catch(Exception) {}
7414 			}
7415 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7416 		default:
7417 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7418 	}
7419 }
7420 +/
7421 
7422 
7423 /++
7424 	A widget specifically designed to hold other widgets.
7425 
7426 	History:
7427 		Added July 1, 2021
7428 +/
7429 class ContainerWidget : Widget {
7430 	this(Widget parent) {
7431 		super(parent);
7432 		this.tabStop = false;
7433 
7434 		version(win32_widgets) {
7435 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
7436 		}
7437 	}
7438 }
7439 
7440 /++
7441 	A widget that takes your widget, puts scroll bars around it, and sends
7442 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
7443 	no effort to automatically scroll or clip its child widgets - it just sends
7444 	the messages.
7445 
7446 
7447 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
7448 	The scroll coordinates are all given in a unit you interpret as you wish. One
7449 	of these units is moved on each press of the arrow buttons and represents the
7450 	smallest amount the user can scroll. The intention is for this to be one line,
7451 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
7452 	in each direction that the user might be interested in.
7453 
7454 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
7455 	This is the amount it jumps when the user pressed page up and page down, or clicks
7456 	in the exposed part of the scroll bar.
7457 
7458 	You should add child content to the ScrollMessageWidget. However, it is important to
7459 	note that the coordinates are always independent of the scroll position! It is YOUR
7460 	responsibility to do any necessary transforms, clipping, etc., while drawing the
7461 	content and interpreting mouse events if they are supposed to change with the scroll.
7462 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
7463 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
7464 	you more control (which can be considerably more efficient and adapted to your actual data)
7465 	at the expense of you also needing to be aware of its reality.
7466 
7467 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
7468 	version 10.3. Maybe this will change in the future.... but for now you must call
7469 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
7470 +/
7471 class ScrollMessageWidget : Widget {
7472 	this(Widget parent) {
7473 		super(parent);
7474 
7475 		container = new Widget(this);
7476 		hsb = new HorizontalScrollbar(this);
7477 		vsb = new VerticalScrollbar(this);
7478 
7479 		hsb.addEventListener("scrolltonextline", {
7480 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
7481 			notify();
7482 		});
7483 		hsb.addEventListener("scrolltopreviousline", {
7484 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
7485 			notify();
7486 		});
7487 		vsb.addEventListener("scrolltonextline", {
7488 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
7489 			notify();
7490 		});
7491 		vsb.addEventListener("scrolltopreviousline", {
7492 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
7493 			notify();
7494 		});
7495 		hsb.addEventListener("scrolltonextpage", {
7496 			hsb.setPosition(hsb.position + hsb.step_);
7497 			notify();
7498 		});
7499 		hsb.addEventListener("scrolltopreviouspage", {
7500 			hsb.setPosition(hsb.position - hsb.step_);
7501 			notify();
7502 		});
7503 		vsb.addEventListener("scrolltonextpage", {
7504 			vsb.setPosition(vsb.position + vsb.step_);
7505 			notify();
7506 		});
7507 		vsb.addEventListener("scrolltopreviouspage", {
7508 			vsb.setPosition(vsb.position - vsb.step_);
7509 			notify();
7510 		});
7511 		hsb.addEventListener("scrolltoposition", (Event event) {
7512 			hsb.setPosition(event.intValue);
7513 			notify();
7514 		});
7515 		vsb.addEventListener("scrolltoposition", (Event event) {
7516 			vsb.setPosition(event.intValue);
7517 			notify();
7518 		});
7519 
7520 
7521 		tabStop = false;
7522 		container.tabStop = false;
7523 		magic = true;
7524 	}
7525 
7526 	private int movementPerButtonClickH_ = 1;
7527 	private int movementPerButtonClickV_ = 1;
7528 	public void movementPerButtonClick(int h, int v) {
7529 		movementPerButtonClickH_ = h;
7530 		movementPerButtonClickV_ = v;
7531 	}
7532 
7533 	/++
7534 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
7535 
7536 
7537 		The defaults for [addDefaultWheelListeners] are:
7538 
7539 			$(LIST
7540 				* Mouse wheel scrolls vertically
7541 				* Alt key + mouse wheel scrolls horiontally
7542 				* Shift + mouse wheel scrolls faster.
7543 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
7544 			)
7545 
7546 		The defaults for [addDefaultKeyboardListeners] are:
7547 
7548 			$(LIST
7549 				* Arrow keys scroll by the given amounts
7550 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
7551 				* Page up and down scroll by the vertical viewable area
7552 				* Home and end scroll to the start and end of the verticle viewable area.
7553 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
7554 			)
7555 
7556 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
7557 
7558 		Params:
7559 			horizontalArrowScrollAmount =
7560 			verticalArrowScrollAmount =
7561 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
7562 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
7563 			shiftMultiplier = multiplies the scroll amount by this when shift is held
7564 	+/
7565 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
7566 		auto _this = this;
7567 
7568 		container.addEventListener((scope KeyDownEvent ke) {
7569 			switch(ke.key) {
7570 				case Key.Left:
7571 					_this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7572 				break;
7573 				case Key.Right:
7574 					_this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7575 				break;
7576 				case Key.Up:
7577 					_this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7578 				break;
7579 				case Key.Down:
7580 					_this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7581 				break;
7582 				case Key.PageUp:
7583 					if(ke.altKey)
7584 						_this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7585 					else
7586 						_this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7587 				break;
7588 				case Key.PageDown:
7589 					if(ke.altKey)
7590 						_this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7591 					else
7592 						_this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7593 				break;
7594 				case Key.Home:
7595 					if(ke.altKey)
7596 						_this.scrollLeft(short.max * 16);
7597 					else
7598 						_this.scrollUp(short.max * 16);
7599 				break;
7600 				case Key.End:
7601 					if(ke.altKey)
7602 						_this.scrollRight(short.max * 16);
7603 					else
7604 						_this.scrollDown(short.max * 16);
7605 				break;
7606 
7607 				default:
7608 					// ignore, not for us.
7609 			}
7610 
7611 		});
7612 	}
7613 
7614 	/// ditto
7615 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
7616 		auto _this = this;
7617 		container.addEventListener((scope ClickEvent ce) {
7618 
7619 			if(ce.target && ce.target.tabStop)
7620 				ce.target.focus();
7621 
7622 			// ctrl is reserved for the application
7623 			if(ce.ctrlKey)
7624 				return;
7625 
7626 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
7627 				return;
7628 
7629 			if(shiftMultiplier == 0 && ce.shiftKey)
7630 				return;
7631 
7632 			if(ce.button == MouseButton.wheelDown) {
7633 				if(ce.altKey)
7634 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7635 				else
7636 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7637 			} else if(ce.button == MouseButton.wheelUp) {
7638 				if(ce.altKey)
7639 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7640 				else
7641 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7642 			}
7643 		});
7644 	}
7645 
7646 	/++
7647 		Scrolls the given amount.
7648 
7649 		History:
7650 			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.
7651 	+/
7652 	void scrollUp(int amount = 1) {
7653 		vsb.setPosition(vsb.position - amount);
7654 		notify();
7655 	}
7656 	/// ditto
7657 	void scrollDown(int amount = 1) {
7658 		vsb.setPosition(vsb.position + amount);
7659 		notify();
7660 	}
7661 	/// ditto
7662 	void scrollLeft(int amount = 1) {
7663 		hsb.setPosition(hsb.position - amount);
7664 		notify();
7665 	}
7666 	/// ditto
7667 	void scrollRight(int amount = 1) {
7668 		hsb.setPosition(hsb.position + amount);
7669 		notify();
7670 	}
7671 
7672 	///
7673 	VerticalScrollbar verticalScrollBar() { return vsb; }
7674 	///
7675 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
7676 
7677 	void notify() {
7678 		static bool insideNotify;
7679 
7680 		if(insideNotify)
7681 			return; // avoid the recursive call, even if it isn't strictly correct
7682 
7683 		insideNotify = true;
7684 		scope(exit) insideNotify = false;
7685 
7686 		this.emit!ScrollEvent();
7687 	}
7688 
7689 	mixin Emits!ScrollEvent;
7690 
7691 	///
7692 	Point position() {
7693 		return Point(hsb.position, vsb.position);
7694 	}
7695 
7696 	///
7697 	void setPosition(int x, int y) {
7698 		hsb.setPosition(x);
7699 		vsb.setPosition(y);
7700 	}
7701 
7702 	///
7703 	void setPageSize(int unitsX, int unitsY) {
7704 		hsb.setStep(unitsX);
7705 		vsb.setStep(unitsY);
7706 	}
7707 
7708 	/// Always call this BEFORE setViewableArea
7709 	void setTotalArea(int width, int height) {
7710 		hsb.setMax(width);
7711 		vsb.setMax(height);
7712 	}
7713 
7714 	/++
7715 		Always set the viewable area AFTER setitng the total area if you are going to change both.
7716 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
7717 		If you need to do that, use [queueRecomputeChildLayout].
7718 	+/
7719 	void setViewableArea(int width, int height) {
7720 
7721 		// actually there IS A need to dothis cuz the max might have changed since then
7722 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
7723 			//return; // no need to do what is already done
7724 		hsb.setViewableArea(width);
7725 		vsb.setViewableArea(height);
7726 
7727 		bool needsNotify = false;
7728 
7729 		// FIXME: if at any point the rhs is outside the scrollbar, we need
7730 		// to reset to 0. but it should remember the old position in case the
7731 		// window resizes again, so it can kinda return ot where it was.
7732 		//
7733 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
7734 		if(width >= hsb.max) {
7735 			// there's plenty of room to display it all so we need to reset to zero
7736 			// FIXME: adjust so it matches the note above
7737 			hsb.setPosition(0);
7738 			needsNotify = true;
7739 		}
7740 		if(height >= vsb.max) {
7741 			// there's plenty of room to display it all so we need to reset to zero
7742 			// FIXME: adjust so it matches the note above
7743 			vsb.setPosition(0);
7744 			needsNotify = true;
7745 		}
7746 		if(needsNotify)
7747 			notify();
7748 	}
7749 
7750 	private bool magic;
7751 	override void addChild(Widget w, int position = int.max) {
7752 		if(magic)
7753 			container.addChild(w, position);
7754 		else
7755 			super.addChild(w, position);
7756 	}
7757 
7758 	override void recomputeChildLayout() {
7759 		if(hsb is null || vsb is null || container is null) return;
7760 
7761 		registerMovement();
7762 
7763 		enum BUTTON_SIZE = 16;
7764 
7765 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
7766 		hsb.x = 0;
7767 		hsb.y = this.height - hsb.height;
7768 
7769 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
7770 		vsb.x = this.width - vsb.width;
7771 		vsb.y = 0;
7772 
7773 		auto vsb_width = vsb.showing ? vsb.width : 0;
7774 		auto hsb_height = hsb.showing ? hsb.height : 0;
7775 
7776 		hsb.width = this.width - vsb_width;
7777 		vsb.height = this.height - hsb_height;
7778 
7779 		hsb.recomputeChildLayout();
7780 		vsb.recomputeChildLayout();
7781 
7782 		if(this.header is null) {
7783 			container.x = 0;
7784 			container.y = 0;
7785 			container.width = this.width - vsb_width;
7786 			container.height = this.height - hsb_height;
7787 			container.recomputeChildLayout();
7788 		} else {
7789 			header.x = 0;
7790 			header.y = 0;
7791 			header.width = this.width - vsb_width;
7792 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
7793 			header.recomputeChildLayout();
7794 
7795 			container.x = 0;
7796 			container.y = scaleWithDpi(BUTTON_SIZE);
7797 			container.width = this.width - vsb_width;
7798 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
7799 			container.recomputeChildLayout();
7800 		}
7801 	}
7802 
7803 	private HorizontalScrollbar hsb;
7804 	private VerticalScrollbar vsb;
7805 	Widget container;
7806 	private Widget header;
7807 
7808 	/++
7809 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
7810 
7811 		History:
7812 			Added September 27, 2021 (dub v10.3)
7813 	+/
7814 	Widget getHeader() {
7815 		if(this.header is null) {
7816 			magic = false;
7817 			scope(exit) magic = true;
7818 			this.header = new Widget(this);
7819 			recomputeChildLayout();
7820 		}
7821 		return this.header;
7822 	}
7823 
7824 	/++
7825 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
7826 
7827 		History:
7828 			Added January 3, 2023 (dub v11.0)
7829 	+/
7830 	void scrollIntoView(Rectangle rect) {
7831 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
7832 
7833 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
7834 
7835 		// the lower right is exclusive normally
7836 		auto test = rect.lowerRight;
7837 		if(test.x > 0) test.x--;
7838 		if(test.y > 0) test.y--;
7839 
7840 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
7841 			// try to scroll only one dimension at a time if we can
7842 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
7843 				setPosition(rect.upperLeft.x, position.y);
7844 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
7845 				setPosition(position.x, rect.upperLeft.y);
7846 		}
7847 
7848 	}
7849 
7850 	override int minHeight() {
7851 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
7852 		if(header !is null)
7853 			min += header.minHeight;
7854 		if(horizontalScrollBar.showing)
7855 			min += horizontalScrollBar.minHeight;
7856 		return min;
7857 	}
7858 
7859 	override int maxHeight() {
7860 		int max = container ? container.maxHeight : int.max;
7861 		if(max == int.max)
7862 			return max;
7863 		if(horizontalScrollBar.showing)
7864 			max += horizontalScrollBar.minHeight;
7865 		return max;
7866 	}
7867 }
7868 
7869 /++
7870 	$(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")
7871 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
7872 +/
7873 version(minigui_screenshots)
7874 @Screenshot("ScrollMessageWidget")
7875 unittest {
7876 	auto window = new Window("ScrollMessageWidget");
7877 
7878 	auto smw = new ScrollMessageWidget(window);
7879 	smw.addDefaultKeyboardListeners();
7880 	smw.addDefaultWheelListeners();
7881 
7882 	window.loop();
7883 }
7884 
7885 /++
7886 	Bypasses automatic layout for its children, using manual positioning and sizing only.
7887 	While you need to manually position them, you must ensure they are inside the StaticLayout's
7888 	bounding box to avoid undefined behavior.
7889 
7890 	You should almost never use this.
7891 +/
7892 class StaticLayout : Layout {
7893 	///
7894 	this(Widget parent) { super(parent); }
7895 	override void recomputeChildLayout() {
7896 		registerMovement();
7897 		foreach(child; children)
7898 			child.recomputeChildLayout();
7899 	}
7900 }
7901 
7902 /++
7903 	Bypasses automatic positioning when being laid out. It is your responsibility to make
7904 	room for this widget in the parent layout.
7905 
7906 	Its children are laid out normally, unless there is exactly one, in which case it takes
7907 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
7908 	can do that with `padding`).
7909 +/
7910 class StaticPosition : Layout {
7911 	///
7912 	this(Widget parent) { super(parent); }
7913 
7914 	override void recomputeChildLayout() {
7915 		registerMovement();
7916 		if(this.children.length == 1) {
7917 			auto child = children[0];
7918 			child.x = 0;
7919 			child.y = 0;
7920 			child.width = this.width;
7921 			child.height = this.height;
7922 			child.recomputeChildLayout();
7923 		} else
7924 		foreach(child; children)
7925 			child.recomputeChildLayout();
7926 	}
7927 
7928 	alias width = typeof(super).width;
7929 	alias height = typeof(super).height;
7930 
7931 	@property int width(int w) @nogc pure @safe nothrow {
7932 		return this._width = w;
7933 	}
7934 
7935 	@property int height(int w) @nogc pure @safe nothrow {
7936 		return this._height = w;
7937 	}
7938 
7939 }
7940 
7941 /++
7942 	FixedPosition is like [StaticPosition], but its coordinates
7943 	are always relative to the viewport, meaning they do not scroll with
7944 	the parent content.
7945 +/
7946 class FixedPosition : StaticPosition {
7947 	///
7948 	this(Widget parent) { super(parent); }
7949 }
7950 
7951 version(win32_widgets)
7952 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
7953 	if(true) {
7954 		// cmd == 0 = menu, cmd == 1 = accelerator
7955 		if(auto item = idm in Action.mapping) {
7956 			foreach(handler; (*item).triggered)
7957 				handler();
7958 		/*
7959 			auto event = new Event("triggered", *item);
7960 			event.button = idm;
7961 			event.dispatch();
7962 		*/
7963 			return 0;
7964 		}
7965 	}
7966 	if(handle)
7967 	if(auto widgetp = handle in Widget.nativeMapping) {
7968 		(*widgetp).handleWmCommand(cmd, idm);
7969 		return 0;
7970 	}
7971 	return 1;
7972 }
7973 
7974 
7975 ///
7976 class Window : Widget {
7977 	int mouseCaptureCount = 0;
7978 	Widget mouseCapturedBy;
7979 	void captureMouse(Widget byWhom) {
7980 		assert(mouseCapturedBy is null || byWhom is mouseCapturedBy);
7981 		mouseCaptureCount++;
7982 		mouseCapturedBy = byWhom;
7983 		win.grabInput();
7984 	}
7985 	void releaseMouseCapture() {
7986 		mouseCaptureCount--;
7987 		mouseCapturedBy = null;
7988 		win.releaseInputGrab();
7989 	}
7990 
7991 	/++
7992 		Sets the window icon which is often seen in title bars and taskbars.
7993 
7994 		History:
7995 			Added April 5, 2022 (dub v10.8)
7996 	+/
7997 	@property void icon(MemoryImage icon) {
7998 		if(win && icon)
7999 			win.icon = icon;
8000 	}
8001 
8002 	///
8003 	@scriptable
8004 	@property bool focused() {
8005 		return win.focused;
8006 	}
8007 
8008 	static class Style : Widget.Style {
8009 		override WidgetBackground background() {
8010 			version(custom_widgets)
8011 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8012 			else version(win32_widgets)
8013 				return WidgetBackground(Color.transparent);
8014 			else static assert(0);
8015 		}
8016 	}
8017 	mixin OverrideStyle!Style;
8018 
8019 	/++
8020 		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.
8021 	+/
8022 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8023 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8024 	}
8025 
8026 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8027 		OperatingSystemFont font;
8028 		if(auto vt = WidgetPainter.visualTheme) {
8029 			font = vt.defaultFontCached(96); // FIXME
8030 		}
8031 
8032 		if(font is null) {
8033 			static int defaultHeightCache;
8034 			if(defaultHeightCache == 0) {
8035 				font = new OperatingSystemFont;
8036 				font.loadDefault;
8037 				defaultHeightCache = font.height();// * 5 / 4;
8038 			}
8039 			return defaultHeightCache;
8040 		}
8041 
8042 		return font.height();// * 5 / 4;
8043 	}
8044 
8045 	Widget focusedWidget;
8046 
8047 	private SimpleWindow win_;
8048 
8049 	@property {
8050 		/++
8051 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8052 
8053 			History:
8054 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8055 		+/
8056 		public SimpleWindow win() {
8057 			return win_;
8058 		}
8059 		///
8060 		protected void win(SimpleWindow w) {
8061 			win_ = w;
8062 		}
8063 	}
8064 
8065 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8066 	this(Widget p) {
8067 		tabStop = false;
8068 		super(p);
8069 	}
8070 
8071 	private void actualRedraw() {
8072 		if(recomputeChildLayoutRequired)
8073 			recomputeChildLayoutEntry();
8074 		if(!showing) return;
8075 
8076 		assert(parentWindow !is null);
8077 
8078 		auto w = drawableWindow;
8079 		if(w is null)
8080 			w = parentWindow.win;
8081 
8082 		if(w.closed())
8083 			return;
8084 
8085 		auto ugh = this.parent;
8086 		int lox, loy;
8087 		while(ugh) {
8088 			lox += ugh.x;
8089 			loy += ugh.y;
8090 			ugh = ugh.parent;
8091 		}
8092 		auto painter = w.draw(true);
8093 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8094 		// RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN);
8095 	}
8096 
8097 
8098 	private bool skipNextChar = false;
8099 
8100 	/++
8101 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8102 
8103 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8104 	+/
8105 	this(SimpleWindow win) {
8106 
8107 		static if(UsingSimpledisplayX11) {
8108 			win.discardAdditionalConnectionState = &discardXConnectionState;
8109 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8110 		}
8111 
8112 		tabStop = false;
8113 		super(null);
8114 		this.win = win;
8115 
8116 		win.addEventListener((Widget.RedrawEvent) {
8117 			if(win.eventQueued!RecomputeEvent) {
8118 				// writeln("skipping");
8119 				return; // let the recompute event do the actual redraw
8120 			}
8121 			this.actualRedraw();
8122 		});
8123 
8124 		win.addEventListener((Widget.RecomputeEvent) {
8125 			recomputeChildLayoutEntry();
8126 			if(win.eventQueued!RedrawEvent)
8127 				return; // let the queued one do it
8128 			else {
8129 				// writeln("drawing");
8130 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8131 			}
8132 		});
8133 
8134 		this.width = win.width;
8135 		this.height = win.height;
8136 		this.parentWindow = this;
8137 
8138 		win.closeQuery = () {
8139 			if(this.emit!ClosingEvent())
8140 				win.close();
8141 		};
8142 		win.onClosing = () {
8143 			this.emit!ClosedEvent();
8144 		};
8145 
8146 		win.windowResized = (int w, int h) {
8147 			this.width = w;
8148 			this.height = h;
8149 			recomputeChildLayout();
8150 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8151 			//version(win32_widgets)
8152 				//InvalidateRect(hwnd, null, true);
8153 			redraw();
8154 		};
8155 
8156 		win.onFocusChange = (bool getting) {
8157 			if(this.focusedWidget) {
8158 				if(getting) {
8159 					this.focusedWidget.emit!FocusEvent();
8160 					this.focusedWidget.emit!FocusInEvent();
8161 				} else {
8162 					this.focusedWidget.emit!BlurEvent();
8163 					this.focusedWidget.emit!FocusOutEvent();
8164 				}
8165 			}
8166 
8167 			if(getting) {
8168 				this.emit!FocusEvent();
8169 				this.emit!FocusInEvent();
8170 			} else {
8171 				this.emit!BlurEvent();
8172 				this.emit!FocusOutEvent();
8173 			}
8174 		};
8175 
8176 		win.onDpiChanged = {
8177 			this.queueRecomputeChildLayout();
8178 			auto event = new DpiChangedEvent(this);
8179 			event.sendDirectly();
8180 
8181 			privateDpiChanged();
8182 		};
8183 
8184 		win.setEventHandlers(
8185 			(MouseEvent e) {
8186 				dispatchMouseEvent(e);
8187 			},
8188 			(KeyEvent e) {
8189 				//writefln("%x   %s", cast(uint) e.key, e.key);
8190 				dispatchKeyEvent(e);
8191 			},
8192 			(dchar e) {
8193 				if(e == 13) e = 10; // hack?
8194 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8195 				dispatchCharEvent(e);
8196 			},
8197 		);
8198 
8199 		addEventListener("char", (Widget, Event ev) {
8200 			if(skipNextChar) {
8201 				ev.preventDefault();
8202 				skipNextChar = false;
8203 			}
8204 		});
8205 
8206 		version(win32_widgets)
8207 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
8208 			if(hwnd !is this.win.impl.hwnd)
8209 				return 1; // we don't care... pass it on
8210 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
8211 			if(mustReturn)
8212 				return ret;
8213 			return 1; // pass it on
8214 		};
8215 
8216 		if(Window.newWindowCreated)
8217 			Window.newWindowCreated(this);
8218 	}
8219 
8220 	version(custom_widgets)
8221 	override void defaultEventHandler_click(ClickEvent event) {
8222 		if(event.target && event.target.tabStop)
8223 			event.target.focus();
8224 	}
8225 
8226 	private static void delegate(Window) newWindowCreated;
8227 
8228 	version(win32_widgets)
8229 	override void paint(WidgetPainter painter) {
8230 		/*
8231 		RECT rect;
8232 		rect.right = this.width;
8233 		rect.bottom = this.height;
8234 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
8235 		*/
8236 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
8237 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
8238 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
8239 		// since the pen is null, to fill the whole space, we need the +1 on both.
8240 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
8241 		SelectObject(painter.impl.hdc, p);
8242 		SelectObject(painter.impl.hdc, b);
8243 	}
8244 	version(custom_widgets)
8245 	override void paint(WidgetPainter painter) {
8246 		auto cs = getComputedStyle();
8247 		painter.fillColor = cs.windowBackgroundColor;
8248 		painter.outlineColor = cs.windowBackgroundColor;
8249 		painter.drawRectangle(Point(0, 0), this.width, this.height);
8250 	}
8251 
8252 
8253 	override void defaultEventHandler_keydown(KeyDownEvent event) {
8254 		Widget _this = event.target;
8255 
8256 		if(event.key == Key.Tab) {
8257 			/* Window tab ordering is a recursive thingy with each group */
8258 
8259 			// FIXME inefficient
8260 			Widget[] helper(Widget p) {
8261 				if(p.hidden)
8262 					return null;
8263 				Widget[] childOrdering;
8264 
8265 				auto children = p.children.dup;
8266 
8267 				while(true) {
8268 					// UIs should be generally small, so gonna brute force it a little
8269 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
8270 
8271 					Widget smallestTab;
8272 					foreach(ref c; children) {
8273 						if(c is null) continue;
8274 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
8275 							smallestTab = c;
8276 							c = null;
8277 						}
8278 					}
8279 					if(smallestTab !is null) {
8280 						if(smallestTab.tabStop && !smallestTab.hidden)
8281 							childOrdering ~= smallestTab;
8282 						if(!smallestTab.hidden)
8283 							childOrdering ~= helper(smallestTab);
8284 					} else
8285 						break;
8286 
8287 				}
8288 
8289 				return childOrdering;
8290 			}
8291 
8292 			Widget[] tabOrdering = helper(this);
8293 
8294 			Widget recipient;
8295 
8296 			if(tabOrdering.length) {
8297 				bool seenThis = false;
8298 				Widget previous;
8299 				foreach(idx, child; tabOrdering) {
8300 					if(child is focusedWidget) {
8301 
8302 						if(event.shiftKey) {
8303 							if(idx == 0)
8304 								recipient = tabOrdering[$-1];
8305 							else
8306 								recipient = tabOrdering[idx - 1];
8307 							break;
8308 						}
8309 
8310 						seenThis = true;
8311 						if(idx + 1 == tabOrdering.length) {
8312 							// we're at the end, either move to the next group
8313 							// or start back over
8314 							recipient = tabOrdering[0];
8315 						}
8316 						continue;
8317 					}
8318 					if(seenThis) {
8319 						recipient = child;
8320 						break;
8321 					}
8322 					previous = child;
8323 				}
8324 			}
8325 
8326 			if(recipient !is null) {
8327 				//  writeln(typeid(recipient));
8328 				recipient.focus();
8329 
8330 				skipNextChar = true;
8331 			}
8332 		}
8333 
8334 		debug if(event.key == Key.F12) {
8335 			if(devTools) {
8336 				devTools.close();
8337 				devTools = null;
8338 			} else {
8339 				devTools = new DevToolWindow(this);
8340 				devTools.show();
8341 			}
8342 		}
8343 	}
8344 
8345 	debug DevToolWindow devTools;
8346 
8347 
8348 	/++
8349 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
8350 
8351 		History:
8352 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
8353 
8354 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
8355 	+/
8356 	this(int width = 500, int height = 500, string title = null) {
8357 		if(title is null) {
8358 			import core.runtime;
8359 			if(Runtime.args.length)
8360 				title = Runtime.args[0];
8361 		}
8362 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus);
8363 
8364 		static if(UsingSimpledisplayX11) {
8365 		///+
8366 		// for input proxy
8367 		auto display = XDisplayConnection.get;
8368 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
8369 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
8370 		XMapWindow(display, inputProxy);
8371 		// writefln("input proxy: 0x%0x", inputProxy);
8372 		this.inputProxy = new SimpleWindow(inputProxy);
8373 
8374 		XEvent lastEvent;
8375 		this.inputProxy.handleNativeEvent = (XEvent ev) {
8376 			lastEvent = ev;
8377 			return 1;
8378 		};
8379 		this.inputProxy.setEventHandlers(
8380 			(MouseEvent e) {
8381 				dispatchMouseEvent(e);
8382 			},
8383 			(KeyEvent e) {
8384 				//writefln("%x   %s", cast(uint) e.key, e.key);
8385 				if(dispatchKeyEvent(e)) {
8386 					// FIXME: i should trap error
8387 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
8388 						auto thing = nw.focusableWindow();
8389 						if(thing && thing.window) {
8390 							lastEvent.xkey.window = thing.window;
8391 							// writeln("sending event ", lastEvent.xkey);
8392 							trapXErrors( {
8393 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
8394 							});
8395 						}
8396 					}
8397 				}
8398 			},
8399 			(dchar e) {
8400 				if(e == 13) e = 10; // hack?
8401 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8402 				dispatchCharEvent(e);
8403 			},
8404 		);
8405 
8406 		this.inputProxy.populateXic();
8407 		// done
8408 		//+/
8409 		}
8410 
8411 
8412 
8413 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
8414 
8415 		this(win);
8416 	}
8417 
8418 	SimpleWindow inputProxy;
8419 
8420 	private SimpleWindow setRequestedInputFocus() {
8421 		return inputProxy;
8422 	}
8423 
8424 	/// ditto
8425 	this(string title, int width = 500, int height = 500) {
8426 		this(width, height, title);
8427 	}
8428 
8429 	///
8430 	@property string title() { return parentWindow.win.title; }
8431 	///
8432 	@property void title(string title) { parentWindow.win.title = title; }
8433 
8434 	///
8435 	@scriptable
8436 	void close() {
8437 		win.close();
8438 		// I synchronize here upon window closing to ensure all child windows
8439 		// get updated too before the event loop. This avoids some random X errors.
8440 		static if(UsingSimpledisplayX11) {
8441 			runInGuiThread( {
8442 				XSync(XDisplayConnection.get, false);
8443 			});
8444 		}
8445 	}
8446 
8447 	bool dispatchKeyEvent(KeyEvent ev) {
8448 		auto wid = focusedWidget;
8449 		if(wid is null)
8450 			wid = this;
8451 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
8452 		event.originalKeyEvent = ev;
8453 		event.key = ev.key;
8454 		event.state = ev.modifierState;
8455 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8456 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8457 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8458 		event.dispatch();
8459 
8460 		return !event.propagationStopped;
8461 	}
8462 
8463 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
8464 	bool dispatchCharEvent(dchar ch) {
8465 		if(focusedWidget) {
8466 			auto event = new CharEvent(focusedWidget, ch);
8467 			event.dispatch();
8468 			return !event.propagationStopped;
8469 		}
8470 		return true;
8471 	}
8472 
8473 	Widget mouseLastOver;
8474 	Widget mouseLastDownOn;
8475 	bool lastWasDoubleClick;
8476 	bool dispatchMouseEvent(MouseEvent ev) {
8477 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
8478 		auto ele = eleR.widget;
8479 
8480 		auto captureEle = ele;
8481 
8482 		if(mouseCapturedBy !is null) {
8483 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
8484 				captureEle = mouseCapturedBy;
8485 		}
8486 
8487 		// a hack to get it relative to the widget.
8488 		eleR.x = ev.x;
8489 		eleR.y = ev.y;
8490 		auto pain = captureEle;
8491 		while(pain) {
8492 			eleR.x -= pain.x;
8493 			eleR.y -= pain.y;
8494 			pain.addScrollPosition(eleR.x, eleR.y);
8495 			pain = pain.parent;
8496 		}
8497 
8498 		void populateMouseEventBase(MouseEventBase event) {
8499 			event.button = ev.button;
8500 			event.buttonLinear = ev.buttonLinear;
8501 			event.state = ev.modifierState;
8502 			event.clientX = eleR.x;
8503 			event.clientY = eleR.y;
8504 
8505 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8506 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8507 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8508 		}
8509 
8510 		if(ev.type == MouseEventType.buttonPressed) {
8511 			{
8512 				auto event = new MouseDownEvent(captureEle);
8513 				populateMouseEventBase(event);
8514 				event.dispatch();
8515 			}
8516 
8517 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
8518 				auto event = new DoubleClickEvent(captureEle);
8519 				populateMouseEventBase(event);
8520 				event.dispatch();
8521 				lastWasDoubleClick = ev.doubleClick;
8522 			} else {
8523 				lastWasDoubleClick = false;
8524 			}
8525 
8526 			mouseLastDownOn = ele;
8527 		} else if(ev.type == MouseEventType.buttonReleased) {
8528 			{
8529 				auto event = new MouseUpEvent(captureEle);
8530 				populateMouseEventBase(event);
8531 				event.dispatch();
8532 			}
8533 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
8534 				auto event = new ClickEvent(captureEle);
8535 				populateMouseEventBase(event);
8536 				event.dispatch();
8537 			}
8538 		} else if(ev.type == MouseEventType.motion) {
8539 			// motion
8540 			{
8541 				auto event = new MouseMoveEvent(captureEle);
8542 				populateMouseEventBase(event); // fills in button which is meaningless but meh
8543 				event.dispatch();
8544 			}
8545 
8546 			if(mouseLastOver !is ele) {
8547 				if(ele !is null) {
8548 					if(!isAParentOf(ele, mouseLastOver)) {
8549 						ele.setDynamicState(DynamicState.hover, true);
8550 						auto event = new MouseEnterEvent(ele);
8551 						event.relatedTarget = mouseLastOver;
8552 						event.sendDirectly();
8553 
8554 						ele.useStyleProperties((scope Widget.Style s) {
8555 							ele.parentWindow.win.cursor = s.cursor;
8556 						});
8557 					}
8558 				}
8559 
8560 				if(mouseLastOver !is null) {
8561 					if(!isAParentOf(mouseLastOver, ele)) {
8562 						mouseLastOver.setDynamicState(DynamicState.hover, false);
8563 						auto event = new MouseLeaveEvent(mouseLastOver);
8564 						event.relatedTarget = ele;
8565 						event.sendDirectly();
8566 					}
8567 				}
8568 
8569 				if(ele !is null) {
8570 					auto event = new MouseOverEvent(ele);
8571 					event.relatedTarget = mouseLastOver;
8572 					event.dispatch();
8573 				}
8574 
8575 				if(mouseLastOver !is null) {
8576 					auto event = new MouseOutEvent(mouseLastOver);
8577 					event.relatedTarget = ele;
8578 					event.dispatch();
8579 				}
8580 
8581 				mouseLastOver = ele;
8582 			}
8583 		}
8584 
8585 		return true; // FIXME: the event default prevented?
8586 	}
8587 
8588 	/++
8589 		Shows the window and runs the application event loop.
8590 
8591 		Blocks until this window is closed.
8592 
8593 		Bugs:
8594 
8595 		$(PITFALL
8596 			You should always have one event loop live for your application.
8597 			If you make two windows in sequence, the second call to loop (or
8598 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
8599 			might fail:
8600 
8601 			---
8602 			// don't do this!
8603 			auto window = new Window();
8604 			window.loop();
8605 
8606 			// or new Window or new MainWindow, all the same
8607 			auto window2 = new SimpleWindow();
8608 			window2.eventLoop(0); // problematic! might crash
8609 			---
8610 
8611 			simpledisplay's current implementation assumes that final cleanup is
8612 			done when the event loop refcount reaches zero. So after the first
8613 			eventLoop returns, when there isn't already another one active, it assumes
8614 			the program will exit soon and cleans up.
8615 
8616 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
8617 			it eventually, but in the mean time, there's an easy solution:
8618 
8619 			---
8620 			// do this
8621 			EventLoop mainEventLoop = EventLoop.get; // just add this line
8622 
8623 			auto window = new Window();
8624 			window.loop();
8625 
8626 			// or any other type of Window etc.
8627 			auto window2 = new Window();
8628 			window2.loop(); // perfectly fine since mainEventLoop still alive
8629 			---
8630 
8631 			By adding a top-level reference to the event loop, it ensures the final cleanup
8632 			is not performed until it goes out of scope too, letting the individual window loops
8633 			work without trouble despite the bug.
8634 		)
8635 
8636 		History:
8637 			The [BlockingMode] parameter was added on December 8, 2021.
8638 			The default behavior is to block until the application quits
8639 			(so all windows have been closed), unless another minigui or
8640 			simpledisplay event loop is already running, in which case it
8641 			will block until this window closes specifically.
8642 	+/
8643 	@scriptable
8644 	void loop(BlockingMode bm = BlockingMode.automatic) {
8645 		if(win.closed)
8646 			return; // otherwise show will throw
8647 		show();
8648 		win.eventLoopWithBlockingMode(bm, 0);
8649 	}
8650 
8651 	private bool firstShow = true;
8652 
8653 	@scriptable
8654 	override void show() {
8655 		bool rd = false;
8656 		if(firstShow) {
8657 			firstShow = false;
8658 			recomputeChildLayout();
8659 			auto f = getFirstFocusable(this); // FIXME: autofocus?
8660 			if(f)
8661 				f.focus();
8662 			redraw();
8663 		}
8664 		win.show();
8665 		super.show();
8666 	}
8667 	@scriptable
8668 	override void hide() {
8669 		win.hide();
8670 		super.hide();
8671 	}
8672 
8673 	static Widget getFirstFocusable(Widget start) {
8674 		if(start is null)
8675 			return null;
8676 
8677 		foreach(widget; &start.focusableWidgets) {
8678 			return widget;
8679 		}
8680 
8681 		return null;
8682 	}
8683 
8684 	static Widget getLastFocusable(Widget start) {
8685 		if(start is null)
8686 			return null;
8687 
8688 		Widget last;
8689 		foreach(widget; &start.focusableWidgets) {
8690 			last = widget;
8691 		}
8692 
8693 		return last;
8694 	}
8695 
8696 
8697 	mixin Emits!ClosingEvent;
8698 	mixin Emits!ClosedEvent;
8699 }
8700 
8701 /++
8702 	History:
8703 		Added January 12, 2022
8704 +/
8705 class DpiChangedEvent : Event {
8706 	enum EventString = "dpichanged";
8707 
8708 	this(Widget target) {
8709 		super(EventString, target);
8710 	}
8711 }
8712 
8713 debug private class DevToolWindow : Window {
8714 	Window p;
8715 
8716 	TextEdit parentList;
8717 	TextEdit logWindow;
8718 	TextLabel clickX, clickY;
8719 
8720 	this(Window p) {
8721 		this.p = p;
8722 		super(400, 300, "Developer Toolbox");
8723 
8724 		logWindow = new TextEdit(this);
8725 		parentList = new TextEdit(this);
8726 
8727 		auto hl = new HorizontalLayout(this);
8728 		clickX = new TextLabel("", TextAlignment.Right, hl);
8729 		clickY = new TextLabel("", TextAlignment.Right, hl);
8730 
8731 		parentListeners ~= p.addEventListener("*", (Event ev) {
8732 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
8733 		});
8734 
8735 		parentListeners ~= p.addEventListener((ClickEvent ev) {
8736 			auto s = ev.srcElement;
8737 
8738 			string list;
8739 
8740 			void addInfo(Widget s) {
8741 				list ~= s.toString();
8742 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
8743 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
8744 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
8745 				list ~= "\n\theight: " ~ toInternal!string(s.height);
8746 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
8747 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
8748 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
8749 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
8750 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
8751 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
8752 			}
8753 
8754 			addInfo(s);
8755 
8756 			s = s.parent;
8757 			while(s) {
8758 				list ~= "\n";
8759 				addInfo(s);
8760 				s = s.parent;
8761 			}
8762 			parentList.content = list;
8763 
8764 			clickX.label = toInternal!string(ev.clientX);
8765 			clickY.label = toInternal!string(ev.clientY);
8766 		});
8767 	}
8768 
8769 	EventListener[] parentListeners;
8770 
8771 	override void close() {
8772 		assert(p !is null);
8773 		foreach(p; parentListeners)
8774 			p.disconnect();
8775 		parentListeners = null;
8776 		p.devTools = null;
8777 		p = null;
8778 		super.close();
8779 	}
8780 
8781 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
8782 		if(ev.key == Key.F12) {
8783 			this.close();
8784 			if(p)
8785 				p.devTools = null;
8786 		} else {
8787 			super.defaultEventHandler_keydown(ev);
8788 		}
8789 	}
8790 
8791 	void log(T...)(T t) {
8792 		string str;
8793 		import std.conv;
8794 		foreach(i; t)
8795 			str ~= to!string(i);
8796 		str ~= "\n";
8797 		logWindow.addText(str);
8798 
8799 		//version(custom_widgets)
8800 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
8801 	}
8802 }
8803 
8804 /++
8805 	A dialog is a transient window that intends to get information from
8806 	the user before being dismissed.
8807 +/
8808 abstract class Dialog : Window {
8809 	///
8810 	this(int width, int height, string title = null) {
8811 		super(width, height, title);
8812 	}
8813 
8814 	///
8815 	abstract void OK();
8816 
8817 	///
8818 	void Cancel() {
8819 		this.close();
8820 	}
8821 }
8822 
8823 /++
8824 	A custom widget similar to the HTML5 <details> tag.
8825 +/
8826 version(none)
8827 class DetailsView : Widget {
8828 
8829 }
8830 
8831 // FIXME: maybe i should expose the other list views Windows offers too
8832 
8833 /++
8834 	A TableView is a widget made to display a table of data strings.
8835 
8836 
8837 	Future_Directions:
8838 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
8839 
8840 		I will add a selection changed event at some point, as well as item clicked events.
8841 	History:
8842 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
8843 	See_Also:
8844 		[ListWidget] which displays a list of strings without additional columns.
8845 +/
8846 class TableView : Widget {
8847 	/++
8848 
8849 	+/
8850 	this(Widget parent) {
8851 		super(parent);
8852 
8853 		version(win32_widgets) {
8854 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
8855 		} else version(custom_widgets) {
8856 			auto smw = new ScrollMessageWidget(this);
8857 			smw.addDefaultKeyboardListeners();
8858 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
8859 			tvwi = new TableViewWidgetInner(this, smw);
8860 		}
8861 	}
8862 
8863 	// FIXME: auto-size columns on double click of header thing like in Windows
8864 	// it need only make the currently displayed things fit well.
8865 
8866 
8867 	private ColumnInfo[] columns;
8868 	private int itemCount;
8869 
8870 	version(custom_widgets) private {
8871 		TableViewWidgetInner tvwi;
8872 	}
8873 
8874 	/// Passed to [setColumnInfo]
8875 	static struct ColumnInfo {
8876 		const(char)[] name; /// the name displayed in the header
8877 		/++
8878 			The default width, in pixels. As a special case, you can set this to -1
8879 			if you want the system to try to automatically size the width to fit visible
8880 			content. If it can't, it will try to pick a sensible default size.
8881 
8882 			Any other negative value is not allowed and may lead to unpredictable results.
8883 
8884 			History:
8885 				The -1 behavior was specified on December 3, 2021. It actually worked before
8886 				anyway on Win32 but now it is a formal feature with partial Linux support.
8887 
8888 			Bugs:
8889 				It doesn't actually attempt to calculate a best-fit width on Linux as of
8890 				December 3, 2021. I do plan to fix this in the future, but Windows is the
8891 				priority right now. At least it doesn't break things when you use it now.
8892 		+/
8893 		int width;
8894 
8895 		/++
8896 			Alignment of the text in the cell. Applies to the header as well as all data in this
8897 			column.
8898 
8899 			Bugs:
8900 				On Windows, the first column ignores this member and is always left aligned.
8901 				You can work around this by inserting a dummy first column with width = 0
8902 				then putting your actual data in the second column, which does respect the
8903 				alignment.
8904 
8905 				This is a quirk of the operating system's implementation going back a very
8906 				long time and is unlikely to ever be fixed.
8907 		+/
8908 		TextAlignment alignment;
8909 
8910 		/++
8911 			After all the pixel widths have been assigned, any left over
8912 			space is divided up among all columns and distributed to according
8913 			to the widthPercent field.
8914 
8915 
8916 			For example, if you have two fields, both with width 50 and one with
8917 			widthPercent of 25 and the other with widthPercent of 75, and the
8918 			container is 200 pixels wide, first both get their width of 50.
8919 			then the 100 remaining pixels are split up, so the one gets a total
8920 			of 75 pixels and the other gets a total of 125.
8921 
8922 			This is automatically applied as the window is resized.
8923 
8924 			If there is not enough space - that is, when a horizontal scrollbar
8925 			needs to appear - there are 0 pixels divided up, and thus everyone
8926 			gets 0. This can cause a column to shrink out of proportion when
8927 			passing the scroll threshold.
8928 
8929 			It is important to still set a fixed width (that is, to populate the
8930 			`width` field) even if you use the percents because that will be the
8931 			default minimum in the event of a scroll bar appearing.
8932 
8933 			The percents total in the column can never exceed 100 or be less than 0.
8934 			Doing this will trigger an assert error.
8935 
8936 			Implementation note:
8937 
8938 			Please note that percentages are only recalculated 1) upon original
8939 			construction and 2) upon resizing the control. If the user adjusts the
8940 			width of a column, the percentage items will not be updated.
8941 
8942 			On the other hand, if the user adjusts the width of a percentage column
8943 			then resizes the window, it is recalculated, meaning their hand adjustment
8944 			is discarded. This specific behavior may change in the future as it is
8945 			arguably a bug, but I'm not certain yet.
8946 
8947 			History:
8948 				Added November 10, 2021 (dub v10.4)
8949 		+/
8950 		int widthPercent;
8951 
8952 
8953 		private int calculatedWidth;
8954 	}
8955 	/++
8956 		Sets the number of columns along with information about the headers.
8957 
8958 		Please note: on Windows, the first column ignores your alignment preference
8959 		and is always left aligned.
8960 	+/
8961 	void setColumnInfo(ColumnInfo[] columns...) {
8962 
8963 		foreach(ref c; columns) {
8964 			c.name = c.name.idup;
8965 		}
8966 		this.columns = columns.dup;
8967 
8968 		updateCalculatedWidth(false);
8969 
8970 		version(custom_widgets) {
8971 			tvwi.header.updateHeaders();
8972 			tvwi.updateScrolls();
8973 		} else version(win32_widgets)
8974 		foreach(i, column; this.columns) {
8975 			LVCOLUMN lvColumn;
8976 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
8977 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
8978 
8979 			auto bfr = WCharzBuffer(column.name);
8980 			lvColumn.pszText = bfr.ptr;
8981 
8982 			if(column.alignment & TextAlignment.Center)
8983 				lvColumn.fmt = LVCFMT_CENTER;
8984 			else if(column.alignment & TextAlignment.Right)
8985 				lvColumn.fmt = LVCFMT_RIGHT;
8986 			else
8987 				lvColumn.fmt = LVCFMT_LEFT;
8988 
8989 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
8990 				throw new WindowsApiException("Insert Column Fail", GetLastError());
8991 		}
8992 	}
8993 
8994 	private int getActualSetSize(size_t i, bool askWindows) {
8995 		version(win32_widgets)
8996 			if(askWindows)
8997 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
8998 		auto w = columns[i].width;
8999 		if(w == -1)
9000 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9001 		return w;
9002 	}
9003 
9004 	private void updateCalculatedWidth(bool informWindows) {
9005 		int padding;
9006 		version(win32_widgets)
9007 			padding = 4;
9008 		int remaining = this.width;
9009 		foreach(i, column; columns)
9010 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9011 		remaining -= padding;
9012 		if(remaining < 0)
9013 			remaining = 0;
9014 
9015 		int percentTotal;
9016 		foreach(i, ref column; columns) {
9017 			percentTotal += column.widthPercent;
9018 
9019 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9020 
9021 			column.calculatedWidth = c;
9022 
9023 			version(win32_widgets)
9024 			if(informWindows)
9025 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9026 		}
9027 
9028 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9029 		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).");
9030 
9031 
9032 	}
9033 
9034 	override void registerMovement() {
9035 		super.registerMovement();
9036 
9037 		updateCalculatedWidth(true);
9038 	}
9039 
9040 	/++
9041 		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.
9042 	+/
9043 	void setItemCount(int count) {
9044 		this.itemCount = count;
9045 		version(custom_widgets) {
9046 			tvwi.updateScrolls();
9047 			redraw();
9048 		} else version(win32_widgets) {
9049 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9050 		}
9051 	}
9052 
9053 	/++
9054 		Clears all items;
9055 	+/
9056 	void clear() {
9057 		this.itemCount = 0;
9058 		this.columns = null;
9059 		version(custom_widgets) {
9060 			tvwi.header.updateHeaders();
9061 			tvwi.updateScrolls();
9062 			redraw();
9063 		} else version(win32_widgets) {
9064 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9065 		}
9066 	}
9067 
9068 	/+
9069 	version(win32_widgets)
9070 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9071 		auto itemId = dis.itemID;
9072 		auto hdc = dis.hDC;
9073 		auto rect = dis.rcItem;
9074 		switch(dis.itemAction) {
9075 			case ODA_DRAWENTIRE:
9076 
9077 				// FIXME: do other items
9078 				// FIXME: do the focus rectangle i guess
9079 				// FIXME: alignment
9080 				// FIXME: column width
9081 				// FIXME: padding left
9082 				// FIXME: check dpi scaling
9083 				// FIXME: don't owner draw unless it is necessary.
9084 
9085 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9086 				RECT itemRect;
9087 				itemRect.top = 1; // subitem idx, 1-based
9088 				itemRect.left = LVIR_BOUNDS;
9089 
9090 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9091 				itemRect.left += padding;
9092 
9093 				getData(itemId, 0, (in char[] data) {
9094 					auto wdata = WCharzBuffer(data);
9095 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9096 
9097 				});
9098 			goto case;
9099 			case ODA_FOCUS:
9100 				if(dis.itemState & ODS_FOCUS)
9101 					DrawFocusRect(hdc, &rect);
9102 			break;
9103 			case ODA_SELECT:
9104 				// itemState & ODS_SELECTED
9105 			break;
9106 			default:
9107 		}
9108 		return 1;
9109 	}
9110 	+/
9111 
9112 	version(win32_widgets) {
9113 		CellStyle last;
9114 		COLORREF defaultColor;
9115 		COLORREF defaultBackground;
9116 	}
9117 
9118 	version(win32_widgets)
9119 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9120 		switch(code) {
9121 			case NM_CUSTOMDRAW:
9122 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9123 				switch(s.nmcd.dwDrawStage) {
9124 					case CDDS_PREPAINT:
9125 						if(getCellStyle is null)
9126 							return 0;
9127 
9128 						mustReturn = true;
9129 						return CDRF_NOTIFYITEMDRAW;
9130 					case CDDS_ITEMPREPAINT:
9131 						mustReturn = true;
9132 						return CDRF_NOTIFYSUBITEMDRAW;
9133 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9134 						mustReturn = true;
9135 
9136 						if(getCellStyle is null) // this SHOULD never happen...
9137 							return 0;
9138 
9139 						if(s.iSubItem == 0) {
9140 							// Windows resets it per row so we'll use item 0 as a chance
9141 							// to capture these for later
9142 							defaultColor = s.clrText;
9143 							defaultBackground = s.clrTextBk;
9144 						}
9145 
9146 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9147 						// if no special style and no reset needed...
9148 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9149 							return 0; // allow default processing to continue
9150 
9151 						last = style;
9152 
9153 						// might still need to reset or use the preference.
9154 
9155 						if(style.flags & CellStyle.Flags.textColorSet)
9156 							s.clrText = style.textColor.asWindowsColorRef;
9157 						else
9158 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
9159 						if(style.flags & CellStyle.Flags.backgroundColorSet)
9160 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
9161 						else
9162 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
9163 
9164 						return CDRF_NEWFONT;
9165 					default:
9166 						return 0;
9167 
9168 				}
9169 			case NM_RETURN: // no need since i subclass keydown
9170 			break;
9171 			case LVN_COLUMNCLICK:
9172 				auto info = cast(LPNMLISTVIEW) hdr;
9173 				this.emit!HeaderClickedEvent(info.iSubItem);
9174 			break;
9175 			case NM_CLICK:
9176 			case NM_DBLCLK:
9177 			case NM_RCLICK:
9178 			case NM_RDBLCLK:
9179 				// the item/subitem is set here and that can be a useful notification
9180 				// even beyond the normal click notification
9181 			break;
9182 			case LVN_GETDISPINFO:
9183 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
9184 				if(info.item.mask & LVIF_TEXT) {
9185 					if(getData) {
9186 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
9187 							auto bfr = WCharzBuffer(dataReceived);
9188 							auto len = info.item.cchTextMax;
9189 							if(bfr.length < len)
9190 								len = cast(typeof(len)) bfr.length;
9191 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
9192 							info.item.pszText[len] = 0;
9193 						});
9194 					} else {
9195 						info.item.pszText[0] = 0;
9196 					}
9197 					//info.item.iItem
9198 					//if(info.item.iSubItem)
9199 				}
9200 			break;
9201 			default:
9202 		}
9203 		return 0;
9204 	}
9205 
9206 	override bool encapsulatedChildren() {
9207 		return true;
9208 	}
9209 
9210 	/++
9211 		Informs the control that content has changed.
9212 
9213 		History:
9214 			Added November 10, 2021 (dub v10.4)
9215 	+/
9216 	void update() {
9217 		version(custom_widgets)
9218 			redraw();
9219 		else {
9220 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
9221 			UpdateWindow(hwnd);
9222 		}
9223 
9224 
9225 	}
9226 
9227 	/++
9228 		Called by the system to request the text content of an individual cell. You
9229 		should pass the text into the provided `sink` delegate. This function will be
9230 		called for each visible cell as-needed when drawing.
9231 	+/
9232 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
9233 
9234 	/++
9235 		Available per-cell style customization options. Use one of the constructors
9236 		provided to set the values conveniently, or default construct it and set individual
9237 		values yourself. Just remember to set the `flags` so your values are actually used.
9238 		If the flag isn't set, the field is ignored and the system default is used instead.
9239 
9240 		This is returned by the [getCellStyle] delegate.
9241 
9242 		Examples:
9243 			---
9244 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
9245 			auto table = new TableView(window);
9246 			// snip: you would set up columns here
9247 
9248 			// this is how you provide data to the table view class
9249 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
9250 				import std.conv;
9251 				sink(to!string(my_data[row][column]));
9252 			};
9253 
9254 			// and this is how you customize the colors
9255 			table.getCellStyle = delegate(int row, int column) {
9256 				return (my_data[row][column] < 0) ?
9257 					TableView.CellStyle(Color.red); // make negative numbers red
9258 					: TableView.CellStyle.init; // leave the rest alone
9259 			};
9260 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
9261 			---
9262 
9263 		History:
9264 			Added November 27, 2021 (dub v10.4)
9265 	+/
9266 	struct CellStyle {
9267 		/// 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.
9268 		this(Color textColor) {
9269 			this.textColor = textColor;
9270 			this.flags |= Flags.textColorSet;
9271 		}
9272 		/// Sets a custom text and background color.
9273 		this(Color textColor, Color backgroundColor) {
9274 			this.textColor = textColor;
9275 			this.backgroundColor = backgroundColor;
9276 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
9277 		}
9278 
9279 		Color textColor;
9280 		Color backgroundColor;
9281 		int flags; /// bitmask of [Flags]
9282 		/// available options to combine into [flags]
9283 		enum Flags {
9284 			textColorSet = 1 << 0,
9285 			backgroundColorSet = 1 << 1,
9286 		}
9287 	}
9288 	/++
9289 		Companion delegate to [getData] that allows you to custom style each
9290 		cell of the table.
9291 
9292 		Returns:
9293 			A [CellStyle] structure that describes the desired style for the
9294 			given cell. `return CellStyle.init` if you want the default style.
9295 
9296 		History:
9297 			Added November 27, 2021 (dub v10.4)
9298 	+/
9299 	CellStyle delegate(int row, int column) getCellStyle;
9300 
9301 	// i want to be able to do things like draw little colored things to show red for negative numbers
9302 	// or background color indicators or even in-cell charts
9303 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
9304 
9305 	/++
9306 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
9307 	+/
9308 	mixin Emits!HeaderClickedEvent;
9309 }
9310 
9311 /++
9312 	This is emitted by the [TableView] when a user clicks on a column header.
9313 
9314 	Its member `columnIndex` has the zero-based index of the column that was clicked.
9315 
9316 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
9317 
9318 	History:
9319 		Added November 27, 2021 (dub v10.4)
9320 +/
9321 class HeaderClickedEvent : Event {
9322 	enum EventString = "HeaderClicked";
9323 	this(Widget target, int columnIndex) {
9324 		this.columnIndex = columnIndex;
9325 		super(EventString, target);
9326 	}
9327 
9328 	/// The index of the column
9329 	int columnIndex;
9330 
9331 	///
9332 	override @property int intValue() {
9333 		return columnIndex;
9334 	}
9335 }
9336 
9337 version(custom_widgets)
9338 private class TableViewWidgetInner : Widget {
9339 
9340 // wrap this thing in a ScrollMessageWidget
9341 
9342 	TableView tvw;
9343 	ScrollMessageWidget smw;
9344 	HeaderWidget header;
9345 
9346 	this(TableView tvw, ScrollMessageWidget smw) {
9347 		this.tvw = tvw;
9348 		this.smw = smw;
9349 		super(smw);
9350 
9351 		this.tabStop = true;
9352 
9353 		header = new HeaderWidget(this, smw.getHeader());
9354 
9355 		smw.addEventListener("scroll", () {
9356 			this.redraw();
9357 			header.redraw();
9358 		});
9359 
9360 
9361 		// I need headers outside the scroll area but rendered on the same line as the up arrow
9362 		// FIXME: add a fixed header to the SMW
9363 	}
9364 
9365 	enum padding = 3;
9366 
9367 	void updateScrolls() {
9368 		int w;
9369 		foreach(idx, column; tvw.columns) {
9370 			if(column.width == 0) continue;
9371 			w += tvw.getActualSetSize(idx, false);// + padding;
9372 		}
9373 		smw.setTotalArea(w, tvw.itemCount);
9374 		columnsWidth = w;
9375 	}
9376 
9377 	private int columnsWidth;
9378 
9379 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
9380 
9381 	override void registerMovement() {
9382 		super.registerMovement();
9383 		// FIXME: actual column width. it might need to be done per-pixel instead of per-colum
9384 		smw.setViewableArea(this.width, this.height / lh);
9385 	}
9386 
9387 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
9388 		int x;
9389 		int y;
9390 
9391 		int row = smw.position.y;
9392 
9393 		foreach(lol; 0 .. this.height / lh) {
9394 			if(row >= tvw.itemCount)
9395 				break;
9396 			x = 0;
9397 			foreach(columnNumber, column; tvw.columns) {
9398 				auto x2 = x + column.calculatedWidth;
9399 				auto smwx = smw.position.x;
9400 
9401 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
9402 					auto startX = x;
9403 					auto endX = x + column.calculatedWidth;
9404 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
9405 						case TextAlignment.Left: startX += padding; break;
9406 						case TextAlignment.Center: startX += padding; endX -= padding; break;
9407 						case TextAlignment.Right: endX -= padding; break;
9408 						default: /* broken */ break;
9409 					}
9410 					if(column.width != 0) // no point drawing an invisible column
9411 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
9412 						// auto clip = painter.setClipRectangle(
9413 
9414 						void dotext(WidgetPainter painter) {
9415 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
9416 						}
9417 
9418 						if(tvw.getCellStyle !is null) {
9419 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
9420 
9421 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
9422 								auto tempPainter = painter;
9423 								tempPainter.fillColor = style.backgroundColor;
9424 								tempPainter.outlineColor = style.backgroundColor;
9425 
9426 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
9427 									Point(endX - smw.position.x, y + lh));
9428 							}
9429 							auto tempPainter = painter;
9430 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
9431 								tempPainter.outlineColor = style.textColor;
9432 
9433 							dotext(tempPainter);
9434 						} else {
9435 							dotext(painter);
9436 						}
9437 					});
9438 				}
9439 
9440 				x += column.calculatedWidth;
9441 			}
9442 			row++;
9443 			y += lh;
9444 		}
9445 		return bounds;
9446 	}
9447 
9448 	static class Style : Widget.Style {
9449 		override WidgetBackground background() {
9450 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
9451 		}
9452 	}
9453 	mixin OverrideStyle!Style;
9454 
9455 	private static class HeaderWidget : Widget {
9456 		this(TableViewWidgetInner tvw, Widget parent) {
9457 			super(parent);
9458 			this.tvw = tvw;
9459 
9460 			this.remainder = new Button("", this);
9461 
9462 			this.addEventListener((scope ClickEvent ev) {
9463 				int header = -1;
9464 				foreach(idx, child; this.children[1 .. $]) {
9465 					if(child is ev.target) {
9466 						header = cast(int) idx;
9467 						break;
9468 					}
9469 				}
9470 
9471 				if(header != -1) {
9472 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
9473 					hce.dispatch();
9474 				}
9475 
9476 			});
9477 		}
9478 
9479 		void updateHeaders() {
9480 			foreach(child; children[1 .. $])
9481 				child.removeWidget();
9482 
9483 			foreach(column; tvw.tvw.columns) {
9484 				// the cast is ok because I dup it above, just the type is never changed.
9485 				// all this is private so it should never get messed up.
9486 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
9487 			}
9488 		}
9489 
9490 		Button remainder;
9491 		TableViewWidgetInner tvw;
9492 
9493 		override void recomputeChildLayout() {
9494 			registerMovement();
9495 			int pos;
9496 			foreach(idx, child; children[1 .. $]) {
9497 				if(idx >= tvw.tvw.columns.length)
9498 					continue;
9499 				child.x = pos;
9500 				child.y = 0;
9501 				child.width = tvw.tvw.columns[idx].calculatedWidth;
9502 				child.height = scaleWithDpi(16);// this.height;
9503 				pos += child.width;
9504 
9505 				child.recomputeChildLayout();
9506 			}
9507 
9508 			if(remainder is null)
9509 				return;
9510 
9511 			remainder.x = pos;
9512 			remainder.y = 0;
9513 			if(pos < this.width)
9514 				remainder.width = this.width - pos;// + 4;
9515 			else
9516 				remainder.width = 0;
9517 			remainder.height = scaleWithDpi(16);
9518 
9519 			remainder.recomputeChildLayout();
9520 		}
9521 
9522 		// for the scrollable children mixin
9523 		Point scrollOrigin() {
9524 			return Point(tvw.smw.position.x, 0);
9525 		}
9526 		void paintFrameAndBackground(WidgetPainter painter) { }
9527 
9528 		mixin ScrollableChildren;
9529 	}
9530 }
9531 
9532 /+
9533 
9534 // given struct / array / number / string / etc, make it viewable and editable
9535 class DataViewerWidget : Widget {
9536 
9537 }
9538 +/
9539 
9540 /++
9541 	A line edit box with an associated label.
9542 
9543 	History:
9544 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
9545 
9546 		```
9547 		Old: ________
9548 
9549 		New:
9550 		____________
9551 		```
9552 
9553 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
9554 
9555 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
9556 		horizontal label but left aligned. You may also consider a [GridLayout].
9557 +/
9558 alias LabeledLineEdit = Labeled!LineEdit;
9559 
9560 /++
9561 	History:
9562 		Added May 19, 2021
9563 +/
9564 class Labeled(T) : Widget {
9565 	///
9566 	this(string label, Widget parent) {
9567 		super(parent);
9568 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
9569 	}
9570 
9571 	/++
9572 		History:
9573 			The alignment parameter was added May 17, 2021
9574 	+/
9575 	this(string label, TextAlignment alignment, Widget parent) {
9576 		super(parent);
9577 		initialize!HorizontalLayout(label, alignment, parent);
9578 	}
9579 
9580 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
9581 		tabStop = false;
9582 		horizontal = is(L == HorizontalLayout);
9583 		auto hl = new L(this);
9584 		if(horizontal) {
9585 			static class SpecialTextLabel : TextLabel {
9586 				this(string label, TextAlignment alignment, Widget parent) {
9587 					super(label, alignment, parent);
9588 				}
9589 
9590 				override int paddingTop() { return 6; }
9591 			}
9592 			this.label = new SpecialTextLabel(label, alignment, hl);
9593 		} else
9594 			this.label = new TextLabel(label, alignment, hl);
9595 		this.lineEdit = new T(hl);
9596 
9597 		this.label.labelFor = this.lineEdit;
9598 	}
9599 
9600 	private bool horizontal;
9601 
9602 	TextLabel label; ///
9603 	T lineEdit; ///
9604 
9605 	override int flexBasisWidth() { return 250; }
9606 
9607 	override int minHeight() {
9608 		return this.children[0].minHeight;
9609 	}
9610 	override int maxHeight() { return minHeight(); }
9611 	override int marginTop() { return 4; }
9612 	override int marginBottom() { return 4; }
9613 
9614 	// FIXME: i should prolly call it value as well as content tbh
9615 
9616 	///
9617 	@property string content() {
9618 		return lineEdit.content;
9619 	}
9620 	///
9621 	@property void content(string c) {
9622 		return lineEdit.content(c);
9623 	}
9624 
9625 	///
9626 	void selectAll() {
9627 		lineEdit.selectAll();
9628 	}
9629 
9630 	override void focus() {
9631 		lineEdit.focus();
9632 	}
9633 }
9634 
9635 /++
9636 	A labeled password edit.
9637 
9638 	History:
9639 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
9640 
9641 		The default parameters for the constructors were also removed on May 19, 2021
9642 +/
9643 alias LabeledPasswordEdit = Labeled!PasswordEdit;
9644 
9645 private string toMenuLabel(string s) {
9646 	string n;
9647 	n.reserve(s.length);
9648 	foreach(c; s)
9649 		if(c == '_')
9650 			n ~= ' ';
9651 		else
9652 			n ~= c;
9653 	return n;
9654 }
9655 
9656 private void autoExceptionHandler(Exception e) {
9657 	messageBox(e.msg);
9658 }
9659 
9660 private void delegate() makeAutomaticHandler(alias fn, T)(T t) {
9661 	static if(is(T : void delegate())) {
9662 		return () {
9663 			try
9664 				t();
9665 			catch(Exception e)
9666 				autoExceptionHandler(e);
9667 		};
9668 	} else static if(is(typeof(fn) Params == __parameters)) {
9669 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
9670 			return () {
9671 				void onOK(string s) {
9672 					member = s;
9673 					try
9674 						t(Params[0](s));
9675 					catch(Exception e)
9676 						autoExceptionHandler(e);
9677 				}
9678 
9679 				if(
9680 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
9681 					|| type == FileDialogType.Save)
9682 				{
9683 					getSaveFileName(&onOK, member, filters, null);
9684 				} else
9685 					getOpenFileName(&onOK, member, filters, null);
9686 			};
9687 		} else {
9688 			struct S {
9689 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
9690 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
9691 				} else mixin(q{
9692 				static foreach(idx, ignore; Params) {
9693 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
9694 				}
9695 				});
9696 			}
9697 			return () {
9698 				dialog((S s) {
9699 					try {
9700 						static if(is(typeof(t) Ret == return)) {
9701 							static if(is(Ret == void)) {
9702 								t(s.tupleof);
9703 							} else {
9704 								auto ret = t(s.tupleof);
9705 								import std.conv;
9706 								messageBox(to!string(ret), "Returned Value");
9707 							}
9708 						}
9709 					} catch(Exception e)
9710 						autoExceptionHandler(e);
9711 				}, null, __traits(identifier, fn));
9712 			};
9713 		}
9714 	}
9715 }
9716 
9717 private template hasAnyRelevantAnnotations(a...) {
9718 	bool helper() {
9719 		bool any;
9720 		foreach(attr; a) {
9721 			static if(is(typeof(attr) == .menu))
9722 				any = true;
9723 			else static if(is(typeof(attr) == .toolbar))
9724 				any = true;
9725 			else static if(is(attr == .separator))
9726 				any = true;
9727 			else static if(is(typeof(attr) == .accelerator))
9728 				any = true;
9729 			else static if(is(typeof(attr) == .hotkey))
9730 				any = true;
9731 			else static if(is(typeof(attr) == .icon))
9732 				any = true;
9733 			else static if(is(typeof(attr) == .label))
9734 				any = true;
9735 			else static if(is(typeof(attr) == .tip))
9736 				any = true;
9737 		}
9738 		return any;
9739 	}
9740 
9741 	enum bool hasAnyRelevantAnnotations = helper();
9742 }
9743 
9744 /++
9745 	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.
9746 +/
9747 class MainWindow : Window {
9748 	///
9749 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
9750 		super(initialWidth, initialHeight, title);
9751 
9752 		_clientArea = new ClientAreaWidget();
9753 		_clientArea.x = 0;
9754 		_clientArea.y = 0;
9755 		_clientArea.width = this.width;
9756 		_clientArea.height = this.height;
9757 		_clientArea.tabStop = false;
9758 
9759 		super.addChild(_clientArea);
9760 
9761 		statusBar = new StatusBar(this);
9762 	}
9763 
9764 	/++
9765 		Adds a menu and toolbar from annotated functions.
9766 
9767 	---
9768         struct Commands {
9769                 @menu("File") {
9770                         void New() {}
9771                         void Open() {}
9772                         void Save() {}
9773                         @separator
9774                         void Exit() @accelerator("Alt+F4") @hotkey('x') {
9775                                 window.close();
9776                         }
9777                 }
9778 
9779                 @menu("Edit") {
9780                         void Undo() {
9781                                 undo();
9782                         }
9783                         @separator
9784                         void Cut() {}
9785                         void Copy() {}
9786                         void Paste() {}
9787                 }
9788 
9789                 @menu("Help") {
9790                         void About() {}
9791                 }
9792         }
9793 
9794         Commands commands;
9795 
9796         window.setMenuAndToolbarFromAnnotatedCode(commands);
9797 	---
9798 
9799 	Note that you can call this function multiple times and it will add the items in order to the given items.
9800 
9801 	+/
9802 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
9803 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9804 	}
9805 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
9806 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9807 	}
9808 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
9809 		Action[] toolbarActions;
9810 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
9811 		Menu[string] mcs;
9812 
9813 		foreach(menu; menuBar.subMenus) {
9814 			mcs[menu.label] = menu;
9815 		}
9816 
9817 		foreach(memberName; __traits(derivedMembers, T)) {
9818 			static if(memberName != "this")
9819 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
9820 				.menu menu;
9821 				.toolbar toolbar;
9822 				bool separator;
9823 				.accelerator accelerator;
9824 				.hotkey hotkey;
9825 				.icon icon;
9826 				string label;
9827 				string tip;
9828 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
9829 					static if(is(typeof(attr) == .menu))
9830 						menu = attr;
9831 					else static if(is(typeof(attr) == .toolbar))
9832 						toolbar = attr;
9833 					else static if(is(attr == .separator))
9834 						separator = true;
9835 					else static if(is(typeof(attr) == .accelerator))
9836 						accelerator = attr;
9837 					else static if(is(typeof(attr) == .hotkey))
9838 						hotkey = attr;
9839 					else static if(is(typeof(attr) == .icon))
9840 						icon = attr;
9841 					else static if(is(typeof(attr) == .label))
9842 						label = attr.label;
9843 					else static if(is(typeof(attr) == .tip))
9844 						tip = attr.tip;
9845 				}
9846 
9847 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
9848 					ushort correctIcon = icon.id; // FIXME
9849 					if(label.length == 0)
9850 						label = memberName.toMenuLabel;
9851 
9852 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName));
9853 
9854 					auto action = new Action(label, correctIcon, handler);
9855 
9856 					if(accelerator.keyString.length) {
9857 						auto ke = KeyEvent.parse(accelerator.keyString);
9858 						action.accelerator = ke;
9859 						accelerators[ke.toStr] = handler;
9860 					}
9861 
9862 					if(toolbar !is .toolbar.init)
9863 						toolbarActions ~= action;
9864 					if(menu !is .menu.init) {
9865 						Menu mc;
9866 						if(menu.name in mcs) {
9867 							mc = mcs[menu.name];
9868 						} else {
9869 							mc = new Menu(menu.name, this);
9870 							menuBar.addItem(mc);
9871 							mcs[menu.name] = mc;
9872 						}
9873 
9874 						if(separator)
9875 							mc.addSeparator();
9876 						mc.addItem(new MenuItem(action));
9877 					}
9878 				}
9879 			}
9880 		}
9881 
9882 		this.menuBar = menuBar;
9883 
9884 		if(toolbarActions.length) {
9885 			auto tb = new ToolBar(toolbarActions, this);
9886 		}
9887 	}
9888 
9889 	void delegate()[string] accelerators;
9890 
9891 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9892 		auto str = event.originalKeyEvent.toStr;
9893 		if(auto acl = str in accelerators)
9894 			(*acl)();
9895 		super.defaultEventHandler_keydown(event);
9896 	}
9897 
9898 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
9899 		super.defaultEventHandler_mouseover(event);
9900 		if(this.statusBar !is null && event.target.statusTip.length)
9901 			this.statusBar.parts[0].content = event.target.statusTip;
9902 		else if(this.statusBar !is null && this.statusTip.length)
9903 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
9904 	}
9905 
9906 	override void addChild(Widget c, int position = int.max) {
9907 		if(auto tb = cast(ToolBar) c)
9908 			version(win32_widgets)
9909 				super.addChild(c, 0);
9910 			else version(custom_widgets)
9911 				super.addChild(c, menuBar ? 1 : 0);
9912 			else static assert(0);
9913 		else
9914 			clientArea.addChild(c, position);
9915 	}
9916 
9917 	ToolBar _toolBar;
9918 	///
9919 	ToolBar toolBar() { return _toolBar; }
9920 	///
9921 	ToolBar toolBar(ToolBar t) {
9922 		_toolBar = t;
9923 		foreach(child; this.children)
9924 			if(child is t)
9925 				return t;
9926 		version(win32_widgets)
9927 			super.addChild(t, 0);
9928 		else version(custom_widgets)
9929 			super.addChild(t, menuBar ? 1 : 0);
9930 		else static assert(0);
9931 		return t;
9932 	}
9933 
9934 	MenuBar _menu;
9935 	///
9936 	MenuBar menuBar() { return _menu; }
9937 	///
9938 	MenuBar menuBar(MenuBar m) {
9939 		if(m is _menu) {
9940 			version(custom_widgets)
9941 				recomputeChildLayout();
9942 			return m;
9943 		}
9944 
9945 		if(_menu !is null) {
9946 			// make sure it is sanely removed
9947 			// FIXME
9948 		}
9949 
9950 		_menu = m;
9951 
9952 		version(win32_widgets) {
9953 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
9954 		} else version(custom_widgets) {
9955 			super.addChild(m, 0);
9956 
9957 		//	clientArea.y = menu.height;
9958 		//	clientArea.height = this.height - menu.height;
9959 
9960 			recomputeChildLayout();
9961 		} else static assert(false);
9962 
9963 		return _menu;
9964 	}
9965 	private Widget _clientArea;
9966 	///
9967 	@property Widget clientArea() { return _clientArea; }
9968 	protected @property void clientArea(Widget wid) {
9969 		_clientArea = wid;
9970 	}
9971 
9972 	private StatusBar _statusBar;
9973 	/++
9974 		Returns the window's [StatusBar]. Be warned it may be `null`.
9975 	+/
9976 	@property StatusBar statusBar() { return _statusBar; }
9977 	/// ditto
9978 	@property void statusBar(StatusBar bar) {
9979 		if(_statusBar !is null)
9980 			_statusBar.removeWidget();
9981 		_statusBar = bar;
9982 		if(bar !is null)
9983 			super.addChild(_statusBar);
9984 	}
9985 }
9986 
9987 /+
9988 	This is really an implementation detail of [MainWindow]
9989 +/
9990 private class ClientAreaWidget : Widget {
9991 	this() {
9992 		this.tabStop = false;
9993 		super(null);
9994 		//sa = new ScrollableWidget(this);
9995 	}
9996 	/*
9997 	ScrollableWidget sa;
9998 	override void addChild(Widget w, int position) {
9999 		if(sa is null)
10000 			super.addChild(w, position);
10001 		else {
10002 			sa.addChild(w, position);
10003 			sa.setContentSize(this.minWidth + 1, this.minHeight);
10004 			writeln(sa.contentWidth, "x", sa.contentHeight);
10005 		}
10006 	}
10007 	*/
10008 }
10009 
10010 /**
10011 	Toolbars are lists of buttons (typically icons) that appear under the menu.
10012 	Each button ought to correspond to a menu item, represented by [Action] objects.
10013 */
10014 class ToolBar : Widget {
10015 	version(win32_widgets) {
10016 		private int idealHeight;
10017 		override int minHeight() { return idealHeight; }
10018 		override int maxHeight() { return idealHeight; }
10019 	} else version(custom_widgets) {
10020 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
10021 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
10022 	} else static assert(false);
10023 	override int heightStretchiness() { return 0; }
10024 
10025 	version(win32_widgets) {
10026 		HIMAGELIST imageListSmall;
10027 		HIMAGELIST imageListLarge;
10028 	}
10029 
10030 	this(Widget parent) {
10031 		this(null, parent);
10032 	}
10033 
10034 	version(win32_widgets)
10035 	void changeIconSize(bool useLarge) {
10036 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
10037 
10038 		/+
10039 		SIZE size;
10040 		import core.sys.windows.commctrl;
10041 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
10042 		idealHeight = size.cy + 4; // the plus 4 is a hack
10043 		+/
10044 
10045 		idealHeight = useLarge ? 34 : 26;
10046 
10047 		if(parent) {
10048 			parent.recomputeChildLayout();
10049 			parent.redraw();
10050 		}
10051 
10052 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
10053 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
10054 	}
10055 
10056 	///
10057 	this(Action[] actions, Widget parent) {
10058 		super(parent);
10059 
10060 		tabStop = false;
10061 
10062 		version(win32_widgets) {
10063 			// so i like how the flat thing looks on windows, but not on wine
10064 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
10065 			// leave it commented
10066 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
10067 
10068 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
10069 
10070 			imageListSmall = ImageList_Create(
10071 				// width, height
10072 				16, 16,
10073 				ILC_COLOR16 | ILC_MASK,
10074 				16 /*numberOfButtons*/, 0);
10075 
10076 			imageListLarge = ImageList_Create(
10077 				// width, height
10078 				24, 24,
10079 				ILC_COLOR16 | ILC_MASK,
10080 				16 /*numberOfButtons*/, 0);
10081 
10082 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
10083 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
10084 
10085 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
10086 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
10087 
10088 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
10089 
10090 			TBBUTTON[] buttons;
10091 
10092 			// FIXME: I_IMAGENONE is if here is no icon
10093 			foreach(action; actions)
10094 				buttons ~= TBBUTTON(
10095 					MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
10096 					action.id,
10097 					TBSTATE_ENABLED, // state
10098 					0, // style
10099 					0, // reserved array, just zero it out
10100 					0, // dwData
10101 					cast(size_t) toWstringzInternal(action.label) // INT_PTR
10102 				);
10103 
10104 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
10105 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
10106 
10107 			/*
10108 			RECT rect;
10109 			GetWindowRect(hwnd, &rect);
10110 			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
10111 			*/
10112 
10113 			dpiChanged(); // to load the things calling changeIconSize the first time
10114 
10115 			assert(idealHeight);
10116 		} else version(custom_widgets) {
10117 			foreach(action; actions)
10118 				new ToolButton(action, this);
10119 		} else static assert(false);
10120 	}
10121 
10122 	override void recomputeChildLayout() {
10123 		.recomputeChildLayout!"width"(this);
10124 	}
10125 
10126 
10127 	version(win32_widgets)
10128 	override protected void dpiChanged() {
10129 		auto sz = scaleWithDpi(16);
10130 		if(sz >= 20)
10131 			changeIconSize(true);
10132 		else
10133 			changeIconSize(false);
10134 	}
10135 }
10136 
10137 enum toolbarIconSize = 24;
10138 
10139 /// 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.
10140 class ToolButton : Button {
10141 	///
10142 	this(string label, Widget parent) {
10143 		super(label, parent);
10144 		tabStop = false;
10145 	}
10146 	///
10147 	this(Action action, Widget parent) {
10148 		super(action.label, parent);
10149 		tabStop = false;
10150 		this.action = action;
10151 	}
10152 
10153 	version(custom_widgets)
10154 	override void defaultEventHandler_click(ClickEvent event) {
10155 		foreach(handler; action.triggered)
10156 			handler();
10157 	}
10158 
10159 	Action action;
10160 
10161 	override int maxWidth() { return toolbarIconSize; }
10162 	override int minWidth() { return toolbarIconSize; }
10163 	override int maxHeight() { return toolbarIconSize; }
10164 	override int minHeight() { return toolbarIconSize; }
10165 
10166 	version(custom_widgets)
10167 	override void paint(WidgetPainter painter) {
10168 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
10169 		painter.outlineColor = Color.black;
10170 
10171 		// I want to get from 16 to 24. that's * 3 / 2
10172 		static assert(toolbarIconSize >= 16);
10173 		enum multiplier = toolbarIconSize / 8;
10174 		enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0);
10175 		switch(action.iconId) {
10176 			case GenericIcons.New:
10177 				painter.fillColor = Color.white;
10178 				painter.drawPolygon(
10179 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor,
10180 					Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor,
10181 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor
10182 				);
10183 			break;
10184 			case GenericIcons.Save:
10185 				painter.fillColor = Color.white;
10186 				painter.outlineColor = Color.black;
10187 				painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10188 
10189 				// the label
10190 				painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor);
10191 
10192 				// the slider
10193 				painter.fillColor = Color.black;
10194 				painter.outlineColor = Color.black;
10195 				painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor);
10196 
10197 				painter.fillColor = Color.white;
10198 				painter.outlineColor = Color.white;
10199 				// the disc window
10200 				painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor);
10201 			break;
10202 			case GenericIcons.Open:
10203 				painter.fillColor = Color.white;
10204 				painter.drawPolygon(
10205 					Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor,
10206 					Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor);
10207 				painter.drawPolygon(
10208 					Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor,
10209 					Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor,
10210 					Point(2, 6) * multiplier / divisor);
10211 				//painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor);
10212 			break;
10213 			case GenericIcons.Copy:
10214 				painter.fillColor = Color.white;
10215 				painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor);
10216 				painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor);
10217 			break;
10218 			case GenericIcons.Cut:
10219 				painter.fillColor = Color.transparent;
10220 				painter.outlineColor = getComputedStyle.foregroundColor();
10221 				painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor);
10222 				painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor);
10223 				painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor);
10224 				painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor);
10225 			break;
10226 			case GenericIcons.Paste:
10227 				painter.fillColor = Color.white;
10228 				painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor);
10229 				painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10230 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor);
10231 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor);
10232 				painter.fillColor = Color.black;
10233 				painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor);
10234 			break;
10235 			case GenericIcons.Help:
10236 				painter.outlineColor = getComputedStyle.foregroundColor();
10237 				painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10238 			break;
10239 			case GenericIcons.Undo:
10240 				painter.fillColor = Color.transparent;
10241 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10242 				painter.outlineColor = Color.black;
10243 				painter.fillColor = Color.black;
10244 				painter.drawPolygon(
10245 					Point(4, 4) * multiplier / divisor,
10246 					Point(8, 2) * multiplier / divisor,
10247 					Point(8, 6) * multiplier / divisor,
10248 					Point(4, 4) * multiplier / divisor,
10249 				);
10250 			break;
10251 			case GenericIcons.Redo:
10252 				painter.fillColor = Color.transparent;
10253 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10254 				painter.outlineColor = Color.black;
10255 				painter.fillColor = Color.black;
10256 				painter.drawPolygon(
10257 					Point(10, 4) * multiplier / divisor,
10258 					Point(6, 2) * multiplier / divisor,
10259 					Point(6, 6) * multiplier / divisor,
10260 					Point(10, 4) * multiplier / divisor,
10261 				);
10262 			break;
10263 			default:
10264 				painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10265 		}
10266 		return bounds;
10267 		});
10268 	}
10269 
10270 }
10271 
10272 
10273 /++
10274 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
10275 +/
10276 class MenuBar : Widget {
10277 	MenuItem[] items;
10278 	Menu[] subMenus;
10279 
10280 	version(win32_widgets) {
10281 		HMENU handle;
10282 		///
10283 		this(Widget parent = null) {
10284 			super(parent);
10285 
10286 			handle = CreateMenu();
10287 			tabStop = false;
10288 		}
10289 	} else version(custom_widgets) {
10290 		///
10291 		this(Widget parent = null) {
10292 			tabStop = false; // these are selected some other way
10293 			super(parent);
10294 		}
10295 
10296 		mixin Padding!q{2};
10297 	} else static assert(false);
10298 
10299 	version(custom_widgets)
10300 	override void paint(WidgetPainter painter) {
10301 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
10302 	}
10303 
10304 	///
10305 	MenuItem addItem(MenuItem item) {
10306 		this.addChild(item);
10307 		items ~= item;
10308 		version(win32_widgets) {
10309 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
10310 		}
10311 		return item;
10312 	}
10313 
10314 
10315 	///
10316 	Menu addItem(Menu item) {
10317 
10318 		subMenus ~= item;
10319 
10320 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
10321 
10322 		addChild(mbItem);
10323 		items ~= mbItem;
10324 
10325 		version(win32_widgets) {
10326 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
10327 		} else version(custom_widgets) {
10328 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
10329 				item.popup(mbItem);
10330 			};
10331 		} else static assert(false);
10332 
10333 		return item;
10334 	}
10335 
10336 	override void recomputeChildLayout() {
10337 		.recomputeChildLayout!"width"(this);
10338 	}
10339 
10340 	override int maxHeight() { return defaultLineHeight + 4; }
10341 	override int minHeight() { return defaultLineHeight + 4; }
10342 }
10343 
10344 
10345 /**
10346 	Status bars appear at the bottom of a MainWindow.
10347 	They are made out of Parts, with a width and content.
10348 
10349 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
10350 
10351 
10352 	sb.parts[0].content = "Status bar text!";
10353 */
10354 class StatusBar : Widget {
10355 	private Part[] partsArray;
10356 	///
10357 	struct Parts {
10358 		@disable this();
10359 		this(StatusBar owner) { this.owner = owner; }
10360 		//@disable this(this);
10361 		///
10362 		@property int length() { return cast(int) owner.partsArray.length; }
10363 		private StatusBar owner;
10364 		private this(StatusBar owner, Part[] parts) {
10365 			this.owner.partsArray = parts;
10366 			this.owner = owner;
10367 		}
10368 		///
10369 		Part opIndex(int p) {
10370 			if(owner.partsArray.length == 0)
10371 				this ~= new StatusBar.Part(0);
10372 			return owner.partsArray[p];
10373 		}
10374 
10375 		///
10376 		Part opOpAssign(string op : "~" )(Part p) {
10377 			assert(owner.partsArray.length < 255);
10378 			p.owner = this.owner;
10379 			p.idx = cast(int) owner.partsArray.length;
10380 			owner.partsArray ~= p;
10381 
10382 			owner.recomputeChildLayout();
10383 
10384 			version(win32_widgets) {
10385 				int[256] pos;
10386 				int cpos;
10387 				foreach(idx, part; owner.partsArray) {
10388 					if(idx + 1 == owner.partsArray.length)
10389 						pos[idx] = -1;
10390 					else {
10391 						cpos += part.currentlyAssignedWidth;
10392 						pos[idx] = cpos;
10393 					}
10394 				}
10395 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
10396 			} else version(custom_widgets) {
10397 				owner.redraw();
10398 			} else static assert(false);
10399 
10400 			return p;
10401 		}
10402 	}
10403 
10404 	private Parts _parts;
10405 	///
10406 	final @property Parts parts() {
10407 		return _parts;
10408 	}
10409 
10410 	/++
10411 
10412 	+/
10413 	static class Part {
10414 		/++
10415 			History:
10416 				Added September 1, 2023 (dub v11.1)
10417 		+/
10418 		enum WidthUnits {
10419 			/++
10420 				Unscaled pixels as they appear on screen.
10421 
10422 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
10423 			+/
10424 			DeviceDependentPixels,
10425 			/++
10426 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
10427 			+/
10428 			DeviceIndependentPixels,
10429 			/++
10430 				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`).
10431 			+/
10432 			ApproximateCharacters,
10433 			/++
10434 				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.
10435 
10436 				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.
10437 			+/
10438 			Proportional
10439 		}
10440 		private WidthUnits units;
10441 		private int width;
10442 		private StatusBar owner;
10443 
10444 		private int currentlyAssignedWidth;
10445 
10446 		/++
10447 			History:
10448 				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.
10449 
10450 				It now allows you to provide your own value for [WidthUnits].
10451 
10452 				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`.
10453 		+/
10454 		this(int w, WidthUnits units = WidthUnits.Proportional) {
10455 			this.units = units;
10456 			this.width = w;
10457 		}
10458 
10459 		/// ditto
10460 		this(int w = 0) {
10461 			if(w == 0)
10462 				this(w, WidthUnits.Proportional);
10463 			else
10464 				this(w, WidthUnits.DeviceDependentPixels);
10465 		}
10466 
10467 		private int idx;
10468 		private string _content;
10469 		///
10470 		@property string content() { return _content; }
10471 		///
10472 		@property void content(string s) {
10473 			version(win32_widgets) {
10474 				_content = s;
10475 				WCharzBuffer bfr = WCharzBuffer(s);
10476 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
10477 			} else version(custom_widgets) {
10478 				if(_content != s) {
10479 					_content = s;
10480 					owner.redraw();
10481 				}
10482 			} else static assert(false);
10483 		}
10484 	}
10485 	string simpleModeContent;
10486 	bool inSimpleMode;
10487 
10488 
10489 	///
10490 	this(Widget parent) {
10491 		super(null); // FIXME
10492 		_parts = Parts(this);
10493 		tabStop = false;
10494 		version(win32_widgets) {
10495 			parentWindow = parent.parentWindow;
10496 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
10497 
10498 			RECT rect;
10499 			GetWindowRect(hwnd, &rect);
10500 			idealHeight = rect.bottom - rect.top;
10501 			assert(idealHeight);
10502 		} else version(custom_widgets) {
10503 		} else static assert(false);
10504 	}
10505 
10506 	override void recomputeChildLayout() {
10507 		int remainingLength = this.width;
10508 
10509 		int proportionalSum;
10510 		int proportionalCount;
10511 		foreach(idx, part; this.partsArray) {
10512 			with(Part.WidthUnits)
10513 			final switch(part.units) {
10514 				case DeviceDependentPixels:
10515 					part.currentlyAssignedWidth = part.width;
10516 					remainingLength -= part.currentlyAssignedWidth;
10517 				break;
10518 				case DeviceIndependentPixels:
10519 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
10520 					remainingLength -= part.currentlyAssignedWidth;
10521 				break;
10522 				case ApproximateCharacters:
10523 					auto cs = getComputedStyle();
10524 					auto font = cs.font;
10525 
10526 					part.currentlyAssignedWidth = font.averageWidth * this.width;
10527 					remainingLength -= part.currentlyAssignedWidth;
10528 				break;
10529 				case Proportional:
10530 					proportionalSum += part.width;
10531 					proportionalCount ++;
10532 				break;
10533 			}
10534 		}
10535 
10536 		foreach(part; this.partsArray) {
10537 			if(part.units == Part.WidthUnits.Proportional) {
10538 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
10539 				if(proportion == 0)
10540 					proportion = 1;
10541 
10542 				if(proportionalSum == 0)
10543 					proportionalSum = proportionalCount;
10544 
10545 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
10546 			}
10547 		}
10548 
10549 		super.recomputeChildLayout();
10550 	}
10551 
10552 	version(win32_widgets)
10553 	override protected void dpiChanged() {
10554 		RECT rect;
10555 		GetWindowRect(hwnd, &rect);
10556 		idealHeight = rect.bottom - rect.top;
10557 		assert(idealHeight);
10558 	}
10559 
10560 	version(custom_widgets)
10561 	override void paint(WidgetPainter painter) {
10562 		auto cs = getComputedStyle();
10563 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10564 		int cpos = 0;
10565 		foreach(idx, part; this.partsArray) {
10566 			auto partWidth = part.currentlyAssignedWidth;
10567 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
10568 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
10569 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
10570 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
10571 
10572 			painter.outlineColor = cs.foregroundColor();
10573 			painter.fillColor = cs.foregroundColor();
10574 
10575 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
10576 			cpos += partWidth;
10577 		}
10578 	}
10579 
10580 
10581 	version(win32_widgets) {
10582 		private int idealHeight;
10583 		override int maxHeight() { return idealHeight; }
10584 		override int minHeight() { return idealHeight; }
10585 	} else version(custom_widgets) {
10586 		override int maxHeight() { return defaultLineHeight + 4; }
10587 		override int minHeight() { return defaultLineHeight + 4; }
10588 	} else static assert(false);
10589 }
10590 
10591 /// Displays an in-progress indicator without known values
10592 version(none)
10593 class IndefiniteProgressBar : Widget {
10594 	version(win32_widgets)
10595 	this(Widget parent) {
10596 		super(parent);
10597 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
10598 		tabStop = false;
10599 	}
10600 	override int minHeight() { return 10; }
10601 }
10602 
10603 /// A progress bar with a known endpoint and completion amount
10604 class ProgressBar : Widget {
10605 	/++
10606 		History:
10607 			Added March 16, 2022 (dub v10.7)
10608 	+/
10609 	this(int min, int max, Widget parent) {
10610 		this(parent);
10611 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
10612 	}
10613 	this(Widget parent) {
10614 		version(win32_widgets) {
10615 			super(parent);
10616 			createWin32Window(this, "msctls_progress32"w, "", 0);
10617 			tabStop = false;
10618 		} else version(custom_widgets) {
10619 			super(parent);
10620 			max = 100;
10621 			step = 10;
10622 			tabStop = false;
10623 		} else static assert(0);
10624 	}
10625 
10626 	version(custom_widgets)
10627 	override void paint(WidgetPainter painter) {
10628 		auto cs = getComputedStyle();
10629 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10630 		painter.fillColor = cs.progressBarColor;
10631 		painter.drawRectangle(Point(0, 0), width * current / max, height);
10632 	}
10633 
10634 
10635 	version(custom_widgets) {
10636 		int current;
10637 		int max;
10638 		int step;
10639 	}
10640 
10641 	///
10642 	void advanceOneStep() {
10643 		version(win32_widgets)
10644 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
10645 		else version(custom_widgets)
10646 			addToPosition(step);
10647 		else static assert(false);
10648 	}
10649 
10650 	///
10651 	void setStepIncrement(int increment) {
10652 		version(win32_widgets)
10653 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
10654 		else version(custom_widgets)
10655 			step = increment;
10656 		else static assert(false);
10657 	}
10658 
10659 	///
10660 	void addToPosition(int amount) {
10661 		version(win32_widgets)
10662 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
10663 		else version(custom_widgets)
10664 			setPosition(current + amount);
10665 		else static assert(false);
10666 	}
10667 
10668 	///
10669 	void setPosition(int pos) {
10670 		version(win32_widgets)
10671 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
10672 		else version(custom_widgets) {
10673 			current = pos;
10674 			if(current > max)
10675 				current = max;
10676 			redraw();
10677 		}
10678 		else static assert(false);
10679 	}
10680 
10681 	///
10682 	void setRange(ushort min, ushort max) {
10683 		version(win32_widgets)
10684 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
10685 		else version(custom_widgets) {
10686 			this.max = max;
10687 		}
10688 		else static assert(false);
10689 	}
10690 
10691 	override int minHeight() { return 10; }
10692 }
10693 
10694 version(custom_widgets)
10695 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
10696 	thisLabel.reserve(label.length);
10697 	bool justSawAmpersand;
10698 	foreach(ch; label) {
10699 		if(justSawAmpersand) {
10700 			justSawAmpersand = false;
10701 			if(ch == '&') {
10702 				goto plain;
10703 			}
10704 			thisAccelerator = ch;
10705 		} else {
10706 			if(ch == '&') {
10707 				justSawAmpersand = true;
10708 				continue;
10709 			}
10710 			plain:
10711 			thisLabel ~= ch;
10712 		}
10713 	}
10714 }
10715 
10716 /++
10717 	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.
10718 
10719 
10720 	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
10721 
10722 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
10723 
10724 	History:
10725 		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.
10726 +/
10727 class Fieldset : Widget {
10728 	// FIXME: on Windows,it doesn't draw the background on the label
10729 	// on X, it doesn't fix the clipping rectangle for it
10730 	version(win32_widgets)
10731 		override int paddingTop() { return defaultLineHeight; }
10732 	else version(custom_widgets)
10733 		override int paddingTop() { return defaultLineHeight + 2; }
10734 	else static assert(false);
10735 	override int paddingBottom() { return 6; }
10736 	override int paddingLeft() { return 6; }
10737 	override int paddingRight() { return 6; }
10738 
10739 	override int marginLeft() { return 6; }
10740 	override int marginRight() { return 6; }
10741 	override int marginTop() { return 2; }
10742 	override int marginBottom() { return 2; }
10743 
10744 	string legend;
10745 
10746 	version(custom_widgets) private dchar accelerator;
10747 
10748 	this(string legend, Widget parent) {
10749 		version(win32_widgets) {
10750 			super(parent);
10751 			this.legend = legend;
10752 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
10753 			tabStop = false;
10754 		} else version(custom_widgets) {
10755 			super(parent);
10756 			tabStop = false;
10757 
10758 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
10759 		} else static assert(0);
10760 	}
10761 
10762 	version(custom_widgets)
10763 	override void paint(WidgetPainter painter) {
10764 		auto dlh = defaultLineHeight;
10765 
10766 		painter.fillColor = Color.transparent;
10767 		auto cs = getComputedStyle();
10768 		painter.pen = Pen(cs.foregroundColor, 1);
10769 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
10770 
10771 		auto tx = painter.textSize(legend);
10772 		painter.outlineColor = Color.transparent;
10773 
10774 		version(Windows) {
10775 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
10776 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
10777 			SelectObject(painter.impl.hdc, b);
10778 		} else static if(UsingSimpledisplayX11) {
10779 			painter.fillColor = getComputedStyle().windowBackgroundColor;
10780 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
10781 		}
10782 		painter.outlineColor = cs.foregroundColor;
10783 		painter.drawText(Point(8, 0), legend);
10784 	}
10785 
10786 	override int maxHeight() {
10787 		auto m = paddingTop() + paddingBottom();
10788 		foreach(child; children) {
10789 			auto mh = child.maxHeight();
10790 			if(mh == int.max)
10791 				return int.max;
10792 			m += mh;
10793 			m += child.marginBottom();
10794 			m += child.marginTop();
10795 		}
10796 		m += 6;
10797 		if(m < minHeight)
10798 			return minHeight;
10799 		return m;
10800 	}
10801 
10802 	override int minHeight() {
10803 		auto m = paddingTop() + paddingBottom();
10804 		foreach(child; children) {
10805 			m += child.minHeight();
10806 			m += child.marginBottom();
10807 			m += child.marginTop();
10808 		}
10809 		return m + 6;
10810 	}
10811 
10812 	override int minWidth() {
10813 		return 6 + cast(int) this.legend.length * 7;
10814 	}
10815 }
10816 
10817 /++
10818 	$(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")
10819 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
10820 +/
10821 version(minigui_screenshots)
10822 @Screenshot("Fieldset")
10823 unittest {
10824 	auto window = new Window(200, 100);
10825 	auto set = new Fieldset("Baby will", window);
10826 	auto option1 = new Radiobox("Eat", set);
10827 	auto option2 = new Radiobox("Cry", set);
10828 	auto option3 = new Radiobox("Sleep", set);
10829 	window.loop();
10830 }
10831 
10832 /// Draws a line
10833 class HorizontalRule : Widget {
10834 	mixin Margin!q{ 2 };
10835 	override int minHeight() { return 2; }
10836 	override int maxHeight() { return 2; }
10837 
10838 	///
10839 	this(Widget parent) {
10840 		super(parent);
10841 	}
10842 
10843 	override void paint(WidgetPainter painter) {
10844 		auto cs = getComputedStyle();
10845 		painter.outlineColor = cs.darkAccentColor;
10846 		painter.drawLine(Point(0, 0), Point(width, 0));
10847 		painter.outlineColor = cs.lightAccentColor;
10848 		painter.drawLine(Point(0, 1), Point(width, 1));
10849 	}
10850 }
10851 
10852 version(minigui_screenshots)
10853 @Screenshot("HorizontalRule")
10854 /++
10855 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
10856 
10857 +/
10858 unittest {
10859 	auto window = new Window(200, 100);
10860 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
10861 	new HorizontalRule(window);
10862 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
10863 	window.loop();
10864 }
10865 
10866 /// ditto
10867 class VerticalRule : Widget {
10868 	mixin Margin!q{ 2 };
10869 	override int minWidth() { return 2; }
10870 	override int maxWidth() { return 2; }
10871 
10872 	///
10873 	this(Widget parent) {
10874 		super(parent);
10875 	}
10876 
10877 	override void paint(WidgetPainter painter) {
10878 		auto cs = getComputedStyle();
10879 		painter.outlineColor = cs.darkAccentColor;
10880 		painter.drawLine(Point(0, 0), Point(0, height));
10881 		painter.outlineColor = cs.lightAccentColor;
10882 		painter.drawLine(Point(1, 0), Point(1, height));
10883 	}
10884 }
10885 
10886 
10887 ///
10888 class Menu : Window {
10889 	void remove() {
10890 		foreach(i, child; parentWindow.children)
10891 			if(child is this) {
10892 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
10893 				break;
10894 			}
10895 		parentWindow.redraw();
10896 
10897 		parentWindow.releaseMouseCapture();
10898 	}
10899 
10900 	///
10901 	void addSeparator() {
10902 		version(win32_widgets)
10903 			AppendMenu(handle, MF_SEPARATOR, 0, null);
10904 		else version(custom_widgets)
10905 			auto hr = new HorizontalRule(this);
10906 		else static assert(0);
10907 	}
10908 
10909 	override int paddingTop() { return 4; }
10910 	override int paddingBottom() { return 4; }
10911 	override int paddingLeft() { return 2; }
10912 	override int paddingRight() { return 2; }
10913 
10914 	version(win32_widgets) {}
10915 	else version(custom_widgets) {
10916 		SimpleWindow dropDown;
10917 		Widget menuParent;
10918 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
10919 			this.menuParent = parent;
10920 
10921 			int w = 150;
10922 			int h = paddingTop + paddingBottom;
10923 			if(this.children.length) {
10924 				// hacking it to get the ideal height out of recomputeChildLayout
10925 				this.width = w;
10926 				this.height = h;
10927 				this.recomputeChildLayout();
10928 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
10929 				h += paddingBottom;
10930 
10931 				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
10932 			}
10933 
10934 			if(offsetY == int.min)
10935 				offsetY = parent.defaultLineHeight;
10936 
10937 			auto coord = parent.globalCoordinates();
10938 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
10939 			this.x = 0;
10940 			this.y = 0;
10941 			this.width = dropDown.width;
10942 			this.height = dropDown.height;
10943 			this.drawableWindow = dropDown;
10944 			this.recomputeChildLayout();
10945 
10946 			static if(UsingSimpledisplayX11)
10947 				XSync(XDisplayConnection.get, 0);
10948 
10949 			dropDown.visibilityChanged = (bool visible) {
10950 				if(visible) {
10951 					this.redraw();
10952 					dropDown.grabInput();
10953 				} else {
10954 					dropDown.releaseInputGrab();
10955 				}
10956 			};
10957 
10958 			dropDown.show();
10959 
10960 			clickListener = this.addEventListener((scope ClickEvent ev) {
10961 				unpopup();
10962 				// need to unlock asap just in case other user handlers block...
10963 				static if(UsingSimpledisplayX11)
10964 					flushGui();
10965 			}, true /* again for asap action */);
10966 		}
10967 
10968 		EventListener clickListener;
10969 	}
10970 	else static assert(false);
10971 
10972 	version(custom_widgets)
10973 	void unpopup() {
10974 		mouseLastOver = mouseLastDownOn = null;
10975 		dropDown.hide();
10976 		if(!menuParent.parentWindow.win.closed) {
10977 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
10978 				maw.setDynamicState(DynamicState.depressed, false);
10979 				maw.setDynamicState(DynamicState.hover, false);
10980 				maw.redraw();
10981 			}
10982 			// menuParent.parentWindow.win.focus();
10983 		}
10984 		clickListener.disconnect();
10985 	}
10986 
10987 	MenuItem[] items;
10988 
10989 	///
10990 	MenuItem addItem(MenuItem item) {
10991 		addChild(item);
10992 		items ~= item;
10993 		version(win32_widgets) {
10994 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
10995 		}
10996 		return item;
10997 	}
10998 
10999 	string label;
11000 
11001 	version(win32_widgets) {
11002 		HMENU handle;
11003 		///
11004 		this(string label, Widget parent) {
11005 			// not actually passing the parent since it effs up the drawing
11006 			super(cast(Widget) null);// parent);
11007 			this.label = label;
11008 			handle = CreatePopupMenu();
11009 		}
11010 	} else version(custom_widgets) {
11011 		///
11012 		this(string label, Widget parent) {
11013 
11014 			if(dropDown) {
11015 				dropDown.close();
11016 			}
11017 			dropDown = new SimpleWindow(
11018 				150, 4,
11019 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
11020 
11021 			this.label = label;
11022 
11023 			super(dropDown);
11024 		}
11025 	} else static assert(false);
11026 
11027 	override int maxHeight() { return defaultLineHeight; }
11028 	override int minHeight() { return defaultLineHeight; }
11029 
11030 	version(custom_widgets)
11031 	override void paint(WidgetPainter painter) {
11032 		this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
11033 	}
11034 }
11035 
11036 /++
11037 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
11038 +/
11039 class MenuItem : MouseActivatedWidget {
11040 	Menu submenu;
11041 
11042 	Action action;
11043 	string label;
11044 
11045 	override int paddingLeft() { return 4; }
11046 
11047 	override int maxHeight() { return defaultLineHeight + 4; }
11048 	override int minHeight() { return defaultLineHeight + 4; }
11049 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
11050 	override int maxWidth() {
11051 		if(cast(MenuBar) parent) {
11052 			return minWidth();
11053 		}
11054 		return int.max;
11055 	}
11056 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
11057 	this(string lbl, Widget parent = null) {
11058 		super(parent);
11059 		//label = lbl; // FIXME
11060 		foreach(char ch; lbl) // FIXME
11061 			if(ch != '&') // FIXME
11062 				label ~= ch; // FIXME
11063 		tabStop = false; // these are selected some other way
11064 	}
11065 
11066 	///
11067 	this(Action action, Widget parent = null) {
11068 		assert(action !is null);
11069 		this(action.label, parent);
11070 		this.action = action;
11071 		tabStop = false; // these are selected some other way
11072 	}
11073 
11074 	version(custom_widgets)
11075 	override void paint(WidgetPainter painter) {
11076 		auto cs = getComputedStyle();
11077 		if(dynamicState & DynamicState.depressed)
11078 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11079 		if(dynamicState & DynamicState.hover)
11080 			painter.outlineColor = cs.activeMenuItemColor;
11081 		else
11082 			painter.outlineColor = cs.foregroundColor;
11083 		painter.fillColor = Color.transparent;
11084 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11085 		if(action && action.accelerator !is KeyEvent.init) {
11086 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
11087 
11088 		}
11089 	}
11090 
11091 	static class Style : Widget.Style {
11092 		override bool variesWithState(ulong dynamicStateFlags) {
11093 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11094 		}
11095 	}
11096 	mixin OverrideStyle!Style;
11097 
11098 	override void defaultEventHandler_triggered(Event event) {
11099 		if(action)
11100 		foreach(handler; action.triggered)
11101 			handler();
11102 
11103 		if(auto pmenu = cast(Menu) this.parent)
11104 			pmenu.remove();
11105 
11106 		super.defaultEventHandler_triggered(event);
11107 	}
11108 }
11109 
11110 version(win32_widgets)
11111 /// A "mouse activiated widget" is really just an abstract variant of button.
11112 class MouseActivatedWidget : Widget {
11113 	@property bool isChecked() {
11114 		assert(hwnd);
11115 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
11116 
11117 	}
11118 	@property void isChecked(bool state) {
11119 		assert(hwnd);
11120 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
11121 
11122 	}
11123 
11124 	override void handleWmCommand(ushort cmd, ushort id) {
11125 		if(cmd == 0) {
11126 			auto event = new Event(EventType.triggered, this);
11127 			event.dispatch();
11128 		}
11129 	}
11130 
11131 	this(Widget parent) {
11132 		super(parent);
11133 	}
11134 }
11135 else version(custom_widgets)
11136 /// ditto
11137 class MouseActivatedWidget : Widget {
11138 	@property bool isChecked() { return isChecked_; }
11139 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
11140 
11141 	private bool isChecked_;
11142 
11143 	this(Widget parent) {
11144 		super(parent);
11145 
11146 		addEventListener((MouseDownEvent ev) {
11147 			if(ev.button == MouseButton.left) {
11148 				setDynamicState(DynamicState.depressed, true);
11149 				setDynamicState(DynamicState.hover, true);
11150 				redraw();
11151 			}
11152 		});
11153 
11154 		addEventListener((MouseUpEvent ev) {
11155 			if(ev.button == MouseButton.left) {
11156 				setDynamicState(DynamicState.depressed, false);
11157 				setDynamicState(DynamicState.hover, false);
11158 				redraw();
11159 			}
11160 		});
11161 
11162 		addEventListener((MouseMoveEvent mme) {
11163 			if(!(mme.state & ModifierState.leftButtonDown)) {
11164 				if(dynamicState_ & DynamicState.depressed) {
11165 					setDynamicState(DynamicState.depressed, false);
11166 					redraw();
11167 				}
11168 			}
11169 		});
11170 	}
11171 
11172 	override void defaultEventHandler_focus(Event ev) {
11173 		super.defaultEventHandler_focus(ev);
11174 		this.redraw();
11175 	}
11176 	override void defaultEventHandler_blur(Event ev) {
11177 		super.defaultEventHandler_blur(ev);
11178 		setDynamicState(DynamicState.depressed, false);
11179 		this.redraw();
11180 	}
11181 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
11182 		super.defaultEventHandler_keydown(ev);
11183 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
11184 			setDynamicState(DynamicState.depressed, true);
11185 			setDynamicState(DynamicState.hover, true);
11186 			this.redraw();
11187 		}
11188 	}
11189 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
11190 		super.defaultEventHandler_keyup(ev);
11191 		if(!(dynamicState & DynamicState.depressed))
11192 			return;
11193 		setDynamicState(DynamicState.depressed, false);
11194 		setDynamicState(DynamicState.hover, false);
11195 		this.redraw();
11196 
11197 		auto event = new Event(EventType.triggered, this);
11198 		event.sendDirectly();
11199 	}
11200 	override void defaultEventHandler_click(ClickEvent ev) {
11201 		super.defaultEventHandler_click(ev);
11202 		if(ev.button == MouseButton.left) {
11203 			auto event = new Event(EventType.triggered, this);
11204 			event.sendDirectly();
11205 		}
11206 	}
11207 
11208 }
11209 else static assert(false);
11210 
11211 /*
11212 /++
11213 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
11214 
11215 	Basically the same as a checkbox.
11216 +/
11217 class OnOffSwitch : MouseActivatedWidget {
11218 
11219 }
11220 */
11221 
11222 /++
11223 	History:
11224 		Added June 15, 2021 (dub v10.1)
11225 +/
11226 struct ImageLabel {
11227 	/++
11228 		Defines a label+image combo used by some widgets.
11229 
11230 		If you provide just a text label, that is all the widget will try to
11231 		display. Or just an image will display just that. If you provide both,
11232 		it may display both text and image side by side or display the image
11233 		and offer text on an input event depending on the widget.
11234 
11235 		History:
11236 			The `alignment` parameter was added on September 27, 2021
11237 	+/
11238 	this(string label, TextAlignment alignment = TextAlignment.Center) {
11239 		this.label = label;
11240 		this.displayFlags = DisplayFlags.displayText;
11241 		this.alignment = alignment;
11242 	}
11243 
11244 	/// ditto
11245 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11246 		this.label = label;
11247 		this.image = image;
11248 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11249 		this.alignment = alignment;
11250 	}
11251 
11252 	/// ditto
11253 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11254 		this.image = image;
11255 		this.displayFlags = DisplayFlags.displayImage;
11256 		this.alignment = alignment;
11257 	}
11258 
11259 	/// ditto
11260 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
11261 		this.label = label;
11262 		this.image = image;
11263 		this.alignment = alignment;
11264 		this.displayFlags = displayFlags;
11265 	}
11266 
11267 	string label;
11268 	MemoryImage image;
11269 
11270 	enum DisplayFlags {
11271 		displayText = 1 << 0,
11272 		displayImage = 1 << 1,
11273 	}
11274 
11275 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11276 
11277 	TextAlignment alignment;
11278 }
11279 
11280 /++
11281 	A basic checked or not checked box with an attached label.
11282 
11283 
11284 	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
11285 
11286 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11287 
11288 	History:
11289 		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.
11290 +/
11291 class Checkbox : MouseActivatedWidget {
11292 	version(win32_widgets) {
11293 		override int maxHeight() { return scaleWithDpi(16); }
11294 		override int minHeight() { return scaleWithDpi(16); }
11295 	} else version(custom_widgets) {
11296 		private enum buttonSize = 16;
11297 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11298 		override int minHeight() { return maxHeight(); }
11299 	} else static assert(0);
11300 
11301 	override int marginLeft() { return 4; }
11302 
11303 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
11304 
11305 	/++
11306 		Just an alias because I keep typing checked out of web habit.
11307 
11308 		History:
11309 			Added May 31, 2021
11310 	+/
11311 	alias checked = isChecked;
11312 
11313 	private string label;
11314 	private dchar accelerator;
11315 
11316 	/++
11317 	+/
11318 	this(string label, Widget parent) {
11319 		this(ImageLabel(label), Appearance.checkbox, parent);
11320 	}
11321 
11322 	/// ditto
11323 	this(string label, Appearance appearance, Widget parent) {
11324 		this(ImageLabel(label), appearance, parent);
11325 	}
11326 
11327 	/++
11328 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
11329 
11330 		History:
11331 			Added June 29, 2021 (dub v10.2)
11332 	+/
11333 	enum Appearance {
11334 		checkbox, /// a normal checkbox
11335 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
11336 		//sliderswitch,
11337 	}
11338 	private Appearance appearance;
11339 
11340 	/// ditto
11341 	private this(ImageLabel label, Appearance appearance, Widget parent) {
11342 		super(parent);
11343 		version(win32_widgets) {
11344 			this.label = label.label;
11345 
11346 			uint extraStyle;
11347 			final switch(appearance) {
11348 				case Appearance.checkbox:
11349 				break;
11350 				case Appearance.pushbutton:
11351 					extraStyle |= BS_PUSHLIKE;
11352 				break;
11353 			}
11354 
11355 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
11356 		} else version(custom_widgets) {
11357 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
11358 		} else static assert(0);
11359 	}
11360 
11361 	version(custom_widgets)
11362 	override void paint(WidgetPainter painter) {
11363 		auto cs = getComputedStyle();
11364 		if(isFocused()) {
11365 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11366 			painter.fillColor = cs.windowBackgroundColor;
11367 			painter.drawRectangle(Point(0, 0), width, height);
11368 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11369 		} else {
11370 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
11371 			painter.fillColor = cs.windowBackgroundColor;
11372 			painter.drawRectangle(Point(0, 0), width, height);
11373 		}
11374 
11375 
11376 		painter.outlineColor = Color.black;
11377 		painter.fillColor = Color.white;
11378 		enum rectOffset = 2;
11379 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
11380 
11381 		if(isChecked) {
11382 			auto size = scaleWithDpi(2);
11383 			painter.pen = Pen(Color.black, size);
11384 			// I'm using height so the checkbox is square
11385 			enum padding = 3;
11386 			painter.drawLine(
11387 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
11388 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
11389 			);
11390 			painter.drawLine(
11391 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
11392 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
11393 			);
11394 
11395 			painter.pen = Pen(Color.black, 1);
11396 		}
11397 
11398 		if(label !is null) {
11399 			painter.outlineColor = cs.foregroundColor();
11400 			painter.fillColor = cs.foregroundColor();
11401 
11402 			// i want the centerline of the text to be aligned with the centerline of the checkbox
11403 			/+
11404 			auto font = cs.font();
11405 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
11406 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
11407 			+/
11408 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
11409 		}
11410 	}
11411 
11412 	override void defaultEventHandler_triggered(Event ev) {
11413 		isChecked = !isChecked;
11414 
11415 		this.emit!(ChangeEvent!bool)(&isChecked);
11416 
11417 		redraw();
11418 	}
11419 
11420 	/// Emits a change event with the checked state
11421 	mixin Emits!(ChangeEvent!bool);
11422 }
11423 
11424 /// Adds empty space to a layout.
11425 class VerticalSpacer : Widget {
11426 	///
11427 	this(Widget parent) {
11428 		super(parent);
11429 	}
11430 }
11431 
11432 /// ditto
11433 class HorizontalSpacer : Widget {
11434 	///
11435 	this(Widget parent) {
11436 		super(parent);
11437 		this.tabStop = false;
11438 	}
11439 }
11440 
11441 
11442 /++
11443 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
11444 
11445 
11446 	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
11447 
11448 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11449 
11450 	History:
11451 		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.
11452 +/
11453 class Radiobox : MouseActivatedWidget {
11454 
11455 	version(win32_widgets) {
11456 		override int maxHeight() { return scaleWithDpi(16); }
11457 		override int minHeight() { return scaleWithDpi(16); }
11458 	} else version(custom_widgets) {
11459 		private enum buttonSize = 16;
11460 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11461 		override int minHeight() { return maxHeight(); }
11462 	} else static assert(0);
11463 
11464 	override int marginLeft() { return 4; }
11465 
11466 	// FIXME: make a label getter
11467 	private string label;
11468 	private dchar accelerator;
11469 
11470 	/++
11471 
11472 	+/
11473 	this(string label, Widget parent) {
11474 		super(parent);
11475 		version(win32_widgets) {
11476 			this.label = label;
11477 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
11478 		} else version(custom_widgets) {
11479 			label.extractWindowsStyleLabel(this.label, this.accelerator);
11480 			height = 16;
11481 			width = height + 4 + cast(int) label.length * 16;
11482 		}
11483 	}
11484 
11485 	version(custom_widgets)
11486 	override void paint(WidgetPainter painter) {
11487 		auto cs = getComputedStyle();
11488 
11489 		if(isFocused) {
11490 			painter.fillColor = cs.windowBackgroundColor;
11491 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11492 		} else {
11493 			painter.fillColor = cs.windowBackgroundColor;
11494 			painter.outlineColor = cs.windowBackgroundColor;
11495 		}
11496 		painter.drawRectangle(Point(0, 0), width, height);
11497 
11498 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11499 
11500 		painter.outlineColor = Color.black;
11501 		painter.fillColor = Color.white;
11502 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
11503 		if(isChecked) {
11504 			painter.outlineColor = Color.black;
11505 			painter.fillColor = Color.black;
11506 			// I'm using height so the checkbox is square
11507 			auto size = scaleWithDpi(2);
11508 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
11509 		}
11510 
11511 		painter.outlineColor = cs.foregroundColor();
11512 		painter.fillColor = cs.foregroundColor();
11513 
11514 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11515 	}
11516 
11517 
11518 	override void defaultEventHandler_triggered(Event ev) {
11519 		isChecked = true;
11520 
11521 		if(this.parent) {
11522 			foreach(child; this.parent.children) {
11523 				if(child is this) continue;
11524 				if(auto rb = cast(Radiobox) child) {
11525 					rb.isChecked = false;
11526 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
11527 					rb.redraw();
11528 				}
11529 			}
11530 		}
11531 
11532 		this.emit!(ChangeEvent!bool)(&this.isChecked);
11533 
11534 		redraw();
11535 	}
11536 
11537 	/// 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.
11538 	mixin Emits!(ChangeEvent!bool);
11539 }
11540 
11541 
11542 /++
11543 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
11544 
11545 
11546 	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
11547 
11548 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11549 
11550 	History:
11551 		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.
11552 +/
11553 class Button : MouseActivatedWidget {
11554 	override int heightStretchiness() { return 3; }
11555 	override int widthStretchiness() { return 3; }
11556 
11557 	/++
11558 		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.
11559 
11560 		History:
11561 			Added July 2, 2021
11562 	+/
11563 	public bool triggersOnMultiClick;
11564 
11565 	private string label_;
11566 	private TextAlignment alignment;
11567 	private dchar accelerator;
11568 
11569 	///
11570 	string label() { return label_; }
11571 	///
11572 	void label(string l) {
11573 		label_ = l;
11574 		version(win32_widgets) {
11575 			WCharzBuffer bfr = WCharzBuffer(l);
11576 			SetWindowTextW(hwnd, bfr.ptr);
11577 		} else version(custom_widgets) {
11578 			redraw();
11579 		}
11580 	}
11581 
11582 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
11583 		super.defaultEventHandler_dblclick(ev);
11584 		if(triggersOnMultiClick) {
11585 			if(ev.button == MouseButton.left) {
11586 				auto event = new Event(EventType.triggered, this);
11587 				event.sendDirectly();
11588 			}
11589 		}
11590 	}
11591 
11592 	private Sprite sprite;
11593 	private int displayFlags;
11594 
11595 	/++
11596 		Creates a push button with the given label, which may be an image or some text.
11597 
11598 		Bugs:
11599 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
11600 
11601 		History:
11602 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
11603 
11604 			The button with label and image will respect requests to show both on Windows as
11605 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
11606 	+/
11607 	this(ImageLabel label, Widget parent) {
11608 		version(win32_widgets) {
11609 			// FIXME: use ideal button size instead
11610 			width = 50;
11611 			height = 30;
11612 			super(parent);
11613 
11614 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
11615 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
11616 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
11617 
11618 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
11619 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
11620 
11621 			if(label.image) {
11622 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
11623 
11624 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
11625 			}
11626 
11627 			this.label = label.label;
11628 		} else version(custom_widgets) {
11629 			width = 50;
11630 			height = 30;
11631 			super(parent);
11632 
11633 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
11634 
11635 			if(label.image) {
11636 				this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
11637 				this.displayFlags = label.displayFlags;
11638 			}
11639 
11640 			this.alignment = label.alignment;
11641 		}
11642 	}
11643 
11644 	///
11645 	this(string label, Widget parent) {
11646 		this(ImageLabel(label), parent);
11647 	}
11648 
11649 	override int minHeight() { return defaultLineHeight + 4; }
11650 
11651 	static class Style : Widget.Style {
11652 		override WidgetBackground background() {
11653 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
11654 
11655 			auto pressed = DynamicState.depressed | DynamicState.hover;
11656 			if((widget.dynamicState & pressed) == pressed) {
11657 				return WidgetBackground(cs.depressedButtonColor());
11658 			} else if(widget.dynamicState & DynamicState.hover) {
11659 				return WidgetBackground(cs.hoveringColor());
11660 			} else {
11661 				return WidgetBackground(cs.buttonColor());
11662 			}
11663 		}
11664 
11665 		override FrameStyle borderStyle() {
11666 			auto pressed = DynamicState.depressed | DynamicState.hover;
11667 			if((widget.dynamicState & pressed) == pressed) {
11668 				return FrameStyle.sunk;
11669 			} else {
11670 				return FrameStyle.risen;
11671 			}
11672 
11673 		}
11674 
11675 		override bool variesWithState(ulong dynamicStateFlags) {
11676 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11677 		}
11678 	}
11679 	mixin OverrideStyle!Style;
11680 
11681 	version(custom_widgets)
11682 	override void paint(WidgetPainter painter) {
11683 		painter.drawThemed(delegate Rectangle(const Rectangle bounds) {
11684 			if(sprite) {
11685 				sprite.drawAt(
11686 					painter,
11687 					bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
11688 					Point(0, 0)
11689 				);
11690 			} else {
11691 				painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
11692 			}
11693 			return bounds;
11694 		});
11695 	}
11696 
11697 	override int flexBasisWidth() {
11698 		version(win32_widgets) {
11699 			SIZE size;
11700 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11701 			if(size.cx == 0)
11702 				goto fallback;
11703 			return size.cx + scaleWithDpi(16);
11704 		}
11705 		fallback:
11706 			return scaleWithDpi(cast(int) label.length * 8 + 16);
11707 	}
11708 
11709 	override int flexBasisHeight() {
11710 		version(win32_widgets) {
11711 			SIZE size;
11712 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11713 			if(size.cy == 0)
11714 				goto fallback;
11715 			return size.cy + scaleWithDpi(6);
11716 		}
11717 		fallback:
11718 			return defaultLineHeight + 4;
11719 	}
11720 }
11721 
11722 /++
11723 	A button with a consistent size, suitable for user commands like OK and CANCEL.
11724 +/
11725 class CommandButton : Button {
11726 	this(string label, Widget parent) {
11727 		super(label, parent);
11728 	}
11729 
11730 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
11731 
11732 	override int maxHeight() {
11733 		return defaultLineHeight + 4;
11734 	}
11735 
11736 	override int maxWidth() {
11737 		return defaultLineHeight * 4;
11738 	}
11739 
11740 	override int marginLeft() { return 12; }
11741 	override int marginRight() { return 12; }
11742 	override int marginTop() { return 12; }
11743 	override int marginBottom() { return 12; }
11744 }
11745 
11746 ///
11747 enum ArrowDirection {
11748 	left, ///
11749 	right, ///
11750 	up, ///
11751 	down ///
11752 }
11753 
11754 ///
11755 version(custom_widgets)
11756 class ArrowButton : Button {
11757 	///
11758 	this(ArrowDirection direction, Widget parent) {
11759 		super("", parent);
11760 		this.direction = direction;
11761 		triggersOnMultiClick = true;
11762 	}
11763 
11764 	private ArrowDirection direction;
11765 
11766 	override int minHeight() { return scaleWithDpi(16); }
11767 	override int maxHeight() { return scaleWithDpi(16); }
11768 	override int minWidth() { return scaleWithDpi(16); }
11769 	override int maxWidth() { return scaleWithDpi(16); }
11770 
11771 	override void paint(WidgetPainter painter) {
11772 		super.paint(painter);
11773 
11774 		auto cs = getComputedStyle();
11775 
11776 		painter.outlineColor = cs.foregroundColor;
11777 		painter.fillColor = cs.foregroundColor;
11778 
11779 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
11780 
11781 		final switch(direction) {
11782 			case ArrowDirection.up:
11783 				painter.drawPolygon(
11784 					scaleWithDpi(Point(2, 10) + offset),
11785 					scaleWithDpi(Point(7, 5) + offset),
11786 					scaleWithDpi(Point(12, 10) + offset),
11787 					scaleWithDpi(Point(2, 10) + offset)
11788 				);
11789 			break;
11790 			case ArrowDirection.down:
11791 				painter.drawPolygon(
11792 					scaleWithDpi(Point(2, 6) + offset),
11793 					scaleWithDpi(Point(7, 11) + offset),
11794 					scaleWithDpi(Point(12, 6) + offset),
11795 					scaleWithDpi(Point(2, 6) + offset)
11796 				);
11797 			break;
11798 			case ArrowDirection.left:
11799 				painter.drawPolygon(
11800 					scaleWithDpi(Point(10, 2) + offset),
11801 					scaleWithDpi(Point(5, 7) + offset),
11802 					scaleWithDpi(Point(10, 12) + offset),
11803 					scaleWithDpi(Point(10, 2) + offset)
11804 				);
11805 			break;
11806 			case ArrowDirection.right:
11807 				painter.drawPolygon(
11808 					scaleWithDpi(Point(6, 2) + offset),
11809 					scaleWithDpi(Point(11, 7) + offset),
11810 					scaleWithDpi(Point(6, 12) + offset),
11811 					scaleWithDpi(Point(6, 2) + offset)
11812 				);
11813 			break;
11814 		}
11815 	}
11816 }
11817 
11818 private
11819 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
11820 	int x, y;
11821 	Widget par = c;
11822 	while(par) {
11823 		x += par.x;
11824 		y += par.y;
11825 		par = par.parent;
11826 	}
11827 	return [x, y];
11828 }
11829 
11830 version(win32_widgets)
11831 private
11832 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
11833 // MapWindowPoints?
11834 	int x, y;
11835 	Widget par = c;
11836 	while(par) {
11837 		x += par.x;
11838 		y += par.y;
11839 		par = par.parent;
11840 		if(par !is null && par.useNativeDrawing())
11841 			break;
11842 	}
11843 	return [x, y];
11844 }
11845 
11846 ///
11847 class ImageBox : Widget {
11848 	private MemoryImage image_;
11849 
11850 	override int widthStretchiness() { return 1; }
11851 	override int heightStretchiness() { return 1; }
11852 	override int widthShrinkiness() { return 1; }
11853 	override int heightShrinkiness() { return 1; }
11854 
11855 	override int flexBasisHeight() {
11856 		return image_.height;
11857 	}
11858 
11859 	override int flexBasisWidth() {
11860 		return image_.width;
11861 	}
11862 
11863 	///
11864 	public void setImage(MemoryImage image){
11865 		this.image_ = image;
11866 		if(this.parentWindow && this.parentWindow.win) {
11867 			if(sprite)
11868 				sprite.dispose();
11869 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
11870 		}
11871 		redraw();
11872 	}
11873 
11874 	/// How to fit the image in the box if they aren't an exact match in size?
11875 	enum HowToFit {
11876 		center, /// centers the image, cropping around all the edges as needed
11877 		crop, /// always draws the image in the upper left, cropping the lower right if needed
11878 		// stretch, /// not implemented
11879 	}
11880 
11881 	private Sprite sprite;
11882 	private HowToFit howToFit_;
11883 
11884 	private Color backgroundColor_;
11885 
11886 	///
11887 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
11888 		this.image_ = image;
11889 		this.tabStop = false;
11890 		this.howToFit_ = howToFit;
11891 		this.backgroundColor_ = backgroundColor;
11892 		super(parent);
11893 		updateSprite();
11894 	}
11895 
11896 	/// ditto
11897 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
11898 		this(image, howToFit, Color.transparent, parent);
11899 	}
11900 
11901 	private void updateSprite() {
11902 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
11903 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
11904 		}
11905 	}
11906 
11907 	override void paint(WidgetPainter painter) {
11908 		updateSprite();
11909 		if(backgroundColor_.a) {
11910 			painter.fillColor = backgroundColor_;
11911 			painter.drawRectangle(Point(0, 0), width, height);
11912 		}
11913 		if(howToFit_ == HowToFit.crop)
11914 			sprite.drawAt(painter, Point(0, 0));
11915 		else if(howToFit_ == HowToFit.center) {
11916 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
11917 		}
11918 	}
11919 }
11920 
11921 ///
11922 class TextLabel : Widget {
11923 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
11924 	override int maxHeight() { return minHeight; }
11925 	override int minWidth() { return 32; }
11926 
11927 	override int flexBasisHeight() { return minHeight(); }
11928 	override int flexBasisWidth() { return defaultTextWidth(label); }
11929 
11930 	string label_;
11931 
11932 	/++
11933 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
11934 
11935 		In practice this means a click on the label will focus the `labelFor`. In future versions
11936 		it will also set screen reader hints but that is not yet implemented.
11937 
11938 		History:
11939 			Added October 3, 2021 (dub v10.4)
11940 	+/
11941 	Widget labelFor;
11942 
11943 	///
11944 	@scriptable
11945 	string label() { return label_; }
11946 
11947 	///
11948 	@scriptable
11949 	void label(string l) {
11950 		label_ = l;
11951 		version(win32_widgets) {
11952 			WCharzBuffer bfr = WCharzBuffer(l);
11953 			SetWindowTextW(hwnd, bfr.ptr);
11954 		} else version(custom_widgets)
11955 			redraw();
11956 	}
11957 
11958 	override void defaultEventHandler_click(scope ClickEvent ce) {
11959 		if(this.labelFor !is null)
11960 			this.labelFor.focus();
11961 	}
11962 
11963 	/++
11964 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
11965 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
11966 	+/
11967 	this(string label, TextAlignment alignment, Widget parent) {
11968 		this.label_ = label;
11969 		this.alignment = alignment;
11970 		this.tabStop = false;
11971 		super(parent);
11972 
11973 		version(win32_widgets)
11974 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
11975 	}
11976 
11977 	/// ditto
11978 	this(string label, Widget parent) {
11979 		this(label, TextAlignment.Right, parent);
11980 	}
11981 
11982 	TextAlignment alignment;
11983 
11984 	version(custom_widgets)
11985 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
11986 		painter.outlineColor = getComputedStyle().foregroundColor;
11987 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
11988 		return bounds;
11989 	}
11990 
11991 }
11992 
11993 version(custom_widgets)
11994 	private struct etc {
11995 		mixin ExperimentalTextComponent;
11996 	}
11997 
11998 version(win32_widgets)
11999 	alias EditableTextWidgetParent = Widget; ///
12000 else version(custom_widgets) {
12001 	version(trash_text) {
12002 		alias EditableTextWidgetParent = ScrollableWidget; ///
12003 	} else {
12004 		alias EditableTextWidgetParent = Widget;
12005 		version=use_new_text_system;
12006 		import arsd.textlayouter;
12007 	}
12008 } else static assert(0);
12009 
12010 version(use_new_text_system)
12011 class TextDisplayHelper : Widget {
12012 	protected TextLayouter l;
12013 	protected ScrollMessageWidget smw;
12014 
12015 	private const(TextLayouter.State)*[] undoStack;
12016 	private const(TextLayouter.State)*[] redoStack;
12017 
12018 	bool readonly;
12019 	bool caretNavigation; // scroll lock can flip this
12020 	bool singleLine;
12021 	bool acceptsTabInput;
12022 
12023 	private Menu ctx;
12024 	override Menu contextMenu(int x, int y) {
12025 		if(ctx is null) {
12026 			ctx = new Menu("Actions", this);
12027 			ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
12028 			ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
12029 			ctx.addSeparator();
12030 			ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
12031 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
12032 			ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
12033 			ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
12034 			ctx.addSeparator();
12035 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
12036 		}
12037 		return ctx;
12038 	}
12039 
12040 	override void defaultEventHandler_blur(Event ev) {
12041 		super.defaultEventHandler_blur(ev);
12042 		if(l.wasMutated()) {
12043 			auto evt = new ChangeEvent!string(this, &this.content);
12044 			evt.dispatch();
12045 			l.clearWasMutatedFlag();
12046 		}
12047 	}
12048 
12049 	private string content() {
12050 		return l.getTextString();
12051 	}
12052 
12053 	void undo() {
12054 		if(undoStack.length) {
12055 			auto state = undoStack[$-1];
12056 			undoStack = undoStack[0 .. $-1];
12057 			undoStack.assumeSafeAppend();
12058 			redoStack ~= l.saveState();
12059 			l.restoreState(state);
12060 			adjustScrollbarSizes();
12061 			scrollForCaret();
12062 			redraw();
12063 			stateCheckpoint = true;
12064 		}
12065 	}
12066 
12067 	void redo() {
12068 		if(redoStack.length) {
12069 			doStateCheckpoint();
12070 			auto state = redoStack[$-1];
12071 			redoStack = redoStack[0 .. $-1];
12072 			redoStack.assumeSafeAppend();
12073 			l.restoreState(state);
12074 			adjustScrollbarSizes();
12075 			scrollForCaret();
12076 			redraw();
12077 			stateCheckpoint = true;
12078 		}
12079 	}
12080 
12081 	void cut() {
12082 		with(l.selection()) {
12083 			if(!isEmpty()) {
12084 				setClipboardText(parentWindow.win, getContentString());
12085 				doStateCheckpoint();
12086 				replaceContent("");
12087 				adjustScrollbarSizes();
12088 				scrollForCaret();
12089 				this.redraw();
12090 			}
12091 		}
12092 
12093 	}
12094 
12095 	void copy() {
12096 		with(l.selection()) {
12097 			if(!isEmpty()) {
12098 				setClipboardText(parentWindow.win, getContentString());
12099 				this.redraw();
12100 			}
12101 		}
12102 	}
12103 
12104 	void paste() {
12105 		getClipboardText(parentWindow.win, (txt) {
12106 			doStateCheckpoint();
12107 			l.selection.replaceContent(txt);
12108 			adjustScrollbarSizes();
12109 			scrollForCaret();
12110 			this.redraw();
12111 		});
12112 	}
12113 
12114 	void deleteContentOfSelection() {
12115 		doStateCheckpoint();
12116 		l.selection.replaceContent("");
12117 		l.selection.setUserXCoordinate();
12118 		adjustScrollbarSizes();
12119 		scrollForCaret();
12120 		redraw();
12121 	}
12122 
12123 	void selectAll() {
12124 		with(l.selection) {
12125 			moveToStartOfDocument();
12126 			setAnchor();
12127 			moveToEndOfDocument();
12128 			setFocus();
12129 		}
12130 		redraw();
12131 	}
12132 
12133 	protected bool stateCheckpoint = true;
12134 
12135 	protected void doStateCheckpoint() {
12136 		if(stateCheckpoint) {
12137 			undoStack ~= l.saveState();
12138 			stateCheckpoint = false;
12139 		}
12140 	}
12141 
12142 	protected void adjustScrollbarSizes() {
12143 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
12144 		auto borderWidth = 2;
12145 		this.smw.setTotalArea(l.width, l.height);
12146 		this.smw.setViewableArea(
12147 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
12148 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
12149 	}
12150 
12151 	protected void scrollForCaret() {
12152 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
12153 		smw.scrollIntoView(l.selection.focusBoundingBox());
12154 	}
12155 
12156 	// FIXME: this should be a theme changed event listener instead
12157 	private BaseVisualTheme currentTheme;
12158 	override void recomputeChildLayout() {
12159 		if(currentTheme is null)
12160 			currentTheme = WidgetPainter.visualTheme;
12161 		if(WidgetPainter.visualTheme !is currentTheme) {
12162 			currentTheme = WidgetPainter.visualTheme;
12163 			auto ds = this.l.defaultStyle;
12164 			if(auto ms = cast(MyTextStyle) ds) {
12165 				auto cs = getComputedStyle();
12166 				auto font = cs.font();
12167 				if(font !is null)
12168 					ms.font_ = font;
12169 				else {
12170 					auto osc = new OperatingSystemFont();
12171 					osc.loadDefault;
12172 					ms.font_ = osc;
12173 				}
12174 			}
12175 		}
12176 		super.recomputeChildLayout();
12177 	}
12178 
12179 	private Point adjustForSingleLine(Point p) {
12180 		if(singleLine)
12181 			return Point(p.x, this.height / 2);
12182 		else
12183 			return p;
12184 	}
12185 
12186 	private bool wordWrapEnabled_;
12187 
12188 	this(TextLayouter l, ScrollMessageWidget parent) {
12189 		this.smw = parent;
12190 
12191 		smw.addDefaultWheelListeners(16, 16, 8);
12192 		smw.movementPerButtonClick(16, 16);
12193 
12194 		this.defaultPadding = Rectangle(2, 2, 2, 2);
12195 
12196 		this.l = l;
12197 		super(parent);
12198 
12199 		smw.addEventListener((scope ScrollEvent se) {
12200 			this.redraw();
12201 		});
12202 
12203 		bool mouseDown;
12204 
12205 		this.addEventListener((scope ResizeEvent re) {
12206 			// FIXME: I should add a method to give this client area width thing
12207 			if(wordWrapEnabled_)
12208 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
12209 
12210 			adjustScrollbarSizes();
12211 			scrollForCaret();
12212 
12213 			this.redraw();
12214 		});
12215 
12216 		this.addEventListener((scope KeyDownEvent kde) {
12217 			switch(kde.key) {
12218 				case Key.Up, Key.Down, Key.Left, Key.Right:
12219 				case Key.Home, Key.End:
12220 					stateCheckpoint = true;
12221 					bool setPosition = false;
12222 					switch(kde.key) {
12223 						case Key.Up: l.selection.moveUp(); break;
12224 						case Key.Down: l.selection.moveDown(); break;
12225 						case Key.Left: l.selection.moveLeft(); setPosition = true; break;
12226 						case Key.Right: l.selection.moveRight(); setPosition = true; break;
12227 						case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
12228 						case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
12229 						default: assert(0);
12230 					}
12231 
12232 					if(kde.shiftKey)
12233 						l.selection.setFocus();
12234 					else
12235 						l.selection.setAnchor();
12236 					if(setPosition)
12237 						l.selection.setUserXCoordinate();
12238 					scrollForCaret();
12239 					redraw();
12240 				break;
12241 				case Key.PageUp, Key.PageDown:
12242 					// FIXME
12243 					scrollForCaret();
12244 				break;
12245 				case Key.Delete:
12246 					if(l.selection.isEmpty()) {
12247 						l.selection.setAnchor();
12248 						l.selection.moveRight();
12249 						l.selection.setFocus();
12250 					}
12251 					deleteContentOfSelection();
12252 					adjustScrollbarSizes();
12253 					scrollForCaret();
12254 				break;
12255 				case Key.Insert:
12256 				break;
12257 				case Key.A:
12258 					if(kde.ctrlKey)
12259 						selectAll();
12260 				break;
12261 				case Key.F:
12262 					// find
12263 				break;
12264 				case Key.Z:
12265 					if(kde.ctrlKey)
12266 						undo();
12267 				break;
12268 				case Key.R:
12269 					if(kde.ctrlKey)
12270 						redo();
12271 				break;
12272 				case Key.X:
12273 					if(kde.ctrlKey)
12274 						cut();
12275 				break;
12276 				case Key.C:
12277 					if(kde.ctrlKey)
12278 						copy();
12279 				break;
12280 				case Key.V:
12281 					if(kde.ctrlKey)
12282 						paste();
12283 				break;
12284 				case Key.F1:
12285 					with(l.selection()) {
12286 						moveToStartOfLine();
12287 						setAnchor();
12288 						moveToEndOfLine();
12289 						moveToIncludeAdjacentEndOfLineMarker();
12290 						setFocus();
12291 						replaceContent("");
12292 					}
12293 
12294 					redraw();
12295 				break;
12296 				/*
12297 				case Key.F2:
12298 					l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
12299 						//(cast(MyTextStyle) old).font,
12300 						font2,
12301 						Color.red)));
12302 					redraw();
12303 				break;
12304 				*/
12305 				case Key.Tab:
12306 					// we process the char event, so don't want to change focus on it
12307 					if(acceptsTabInput)
12308 						kde.preventDefault();
12309 				break;
12310 				default:
12311 			}
12312 		});
12313 
12314 		Point downAt;
12315 
12316 		static if(UsingSimpledisplayX11)
12317 		this.addEventListener((scope ClickEvent ce) {
12318 			if(ce.button == MouseButton.middle) {
12319 				parentWindow.win.getPrimarySelection((txt) {
12320 					l.selection.replaceContent(txt);
12321 					redraw();
12322 				});
12323 			}
12324 		});
12325 
12326 		this.addEventListener((scope MouseDownEvent ce) {
12327 			if(ce.button == MouseButton.left) {
12328 				downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12329 				l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
12330 				l.selection.setAnchor();
12331 				mouseDown = true;
12332 				parentWindow.captureMouse(this);
12333 				this.redraw();
12334 			} else if(ce.button == MouseButton.right) {
12335 				this.showContextMenu(ce.clientX, ce.clientY);
12336 			}
12337 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12338 		});
12339 
12340 		Timer autoscrollTimer;
12341 		int autoscrollDirection;
12342 		int autoscrollAmount;
12343 
12344 		void autoscroll() {
12345 			switch(autoscrollDirection) {
12346 				case 0: smw.scrollUp(autoscrollAmount); break;
12347 				case 1: smw.scrollDown(autoscrollAmount); break;
12348 				case 2: smw.scrollLeft(autoscrollAmount); break;
12349 				case 3: smw.scrollRight(autoscrollAmount); break;
12350 				default: assert(0);
12351 			}
12352 
12353 			this.redraw();
12354 		}
12355 
12356 		void setAutoscrollTimer(int direction, int amount) {
12357 			if(autoscrollTimer is null) {
12358 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
12359 			}
12360 
12361 			autoscrollDirection = direction;
12362 			autoscrollAmount = amount;
12363 		}
12364 
12365 		void stopAutoscrollTimer() {
12366 			if(autoscrollTimer !is null) {
12367 				autoscrollTimer.dispose();
12368 				autoscrollTimer = null;
12369 			}
12370 			autoscrollAmount = 0;
12371 			autoscrollDirection = 0;
12372 		}
12373 
12374 		this.addEventListener((scope MouseMoveEvent ce) {
12375 			if(mouseDown) {
12376 				auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12377 
12378 				// FIXME: when scrolling i actually do want a timer.
12379 				// i also want a zone near the sides of the window where i can auto scroll
12380 
12381 				auto scrollMultiplier = scaleWithDpi(16);
12382 				auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
12383 
12384 				if(!singleLine && movedTo.y < 4) {
12385 					setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
12386 				} else
12387 				if(!singleLine && (movedTo.y + 6) > this.height) {
12388 					setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
12389 				} else
12390 				if(movedTo.x < 4) {
12391 					setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
12392 				} else
12393 				if((movedTo.x + 6) > this.width) {
12394 					setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
12395 				} else
12396 					stopAutoscrollTimer();
12397 
12398 				l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
12399 				l.selection.setFocus();
12400 				this.redraw();
12401 			}
12402 		});
12403 
12404 		this.addEventListener((scope MouseUpEvent ce) {
12405 			// FIXME: assert primary selection
12406 			if(mouseDown && ce.button == MouseButton.left) {
12407 				stateCheckpoint = true;
12408 				//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
12409 				//l.selection.setFocus();
12410 				mouseDown = false;
12411 				parentWindow.releaseMouseCapture();
12412 				stopAutoscrollTimer();
12413 				this.redraw();
12414 			}
12415 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12416 		});
12417 
12418 		this.addEventListener((scope CharEvent ce) {
12419 			if(readonly)
12420 				return;
12421 			if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
12422 				return; // skip the ctrl+x characters we don't care about as plain text
12423 
12424 			if(singleLine && ce.character == '\n')
12425 				return;
12426 			if(!acceptsTabInput && ce.character == '\t')
12427 				return;
12428 
12429 			doStateCheckpoint();
12430 
12431 			char[4] buffer;
12432 			import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web
12433 			auto stride = encode(buffer, ce.character);
12434 			l.selection.replaceContent(buffer[0 .. stride]);
12435 			l.selection.setUserXCoordinate();
12436 			adjustScrollbarSizes();
12437 			scrollForCaret();
12438 			redraw();
12439 		});
12440 	}
12441 
12442 	static class Style : Widget.Style {
12443 		override WidgetBackground background() {
12444 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
12445 		}
12446 
12447 		override Color foregroundColor() {
12448 			return WidgetPainter.visualTheme.foregroundColor;
12449 		}
12450 
12451 		override FrameStyle borderStyle() {
12452 			return FrameStyle.sunk;
12453 		}
12454 
12455 		override MouseCursor cursor() {
12456 			return GenericCursor.Text;
12457 		}
12458 	}
12459 	mixin OverrideStyle!Style;
12460 
12461 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
12462 	override int maxHeight() {
12463 		if(singleLine)
12464 			return minHeight;
12465 		else
12466 			return super.maxHeight();
12467 	}
12468 
12469 	void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
12470 		painter.drawText(upperLeft, text);
12471 	}
12472 
12473 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12474 		//painter.setFont(font);
12475 
12476 		auto cs = getComputedStyle();
12477 		auto defaultColor = cs.foregroundColor;
12478 
12479 		auto old = painter.setClipRectangle(bounds);
12480 		scope(exit) painter.setClipRectangle(old);
12481 
12482 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
12483 			//writeln("Segment: ", txt);
12484 			assert(style !is null);
12485 
12486 			auto myStyle = cast(MyTextStyle) style;
12487 			assert(myStyle !is null);
12488 
12489 			painter.setFont(myStyle.font);
12490 			// defaultColor = myStyle.color; // FIXME: so wrong
12491 
12492 			if(info.selections && info.boundingBox.width > 0) {
12493 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
12494 				painter.fillColor = color;
12495 				painter.outlineColor = color;
12496 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
12497 				painter.outlineColor = cs.selectionForegroundColor;
12498 				//painter.fillColor = Color.white;
12499 			} else {
12500 				painter.outlineColor = defaultColor;
12501 			}
12502 
12503 			if(this.isFocused)
12504 			foreach(idx, caret; carets) {
12505 				if(idx == 0)
12506 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
12507 				painter.drawLine(
12508 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
12509 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
12510 				);
12511 			}
12512 
12513 			if(txt.stripInternal.length) {
12514 				drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
12515 			}
12516 
12517 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
12518 				return false;
12519 			} else {
12520 				return true;
12521 			}
12522 		}, Rectangle(smw.position(), bounds.size));
12523 
12524 		/+
12525 		int place = 0;
12526 		int y = 75;
12527 		foreach(width; widths) {
12528 			painter.fillColor = Color.red;
12529 			painter.drawRectangle(Point(place, y), Size(width, 75));
12530 			//y += 15;
12531 			place += width;
12532 		}
12533 		+/
12534 
12535 		return bounds;
12536 	}
12537 
12538 	static class MyTextStyle : TextStyle {
12539 		OperatingSystemFont font_;
12540 		this(OperatingSystemFont font, bool passwordMode = false) {
12541 			this.font_ = font;
12542 		}
12543 
12544 		override OperatingSystemFont font() {
12545 			return font_;
12546 		}
12547 	}
12548 }
12549 
12550 /+
12551 version(use_new_text_system)
12552 class TextWidget : Widget {
12553 	TextLayouter l;
12554 	ScrollMessageWidget smw;
12555 	TextDisplayHelper helper;
12556 	this(TextLayouter l, Widget parent) {
12557 		this.l = l;
12558 		super(parent);
12559 
12560 		smw = new ScrollMessageWidget(this);
12561 		//smw.horizontalScrollBar.hide;
12562 		//smw.verticalScrollBar.hide;
12563 		smw.addDefaultWheelListeners(16, 16, 8);
12564 		smw.movementPerButtonClick(16, 16);
12565 		helper = new TextDisplayHelper(l, smw);
12566 
12567 		// no need to do this here since there's gonna be a resize
12568 		// event immediately before any drawing
12569 		// smw.setTotalArea(l.width, l.height);
12570 		smw.setViewableArea(
12571 			this.width - this.paddingLeft - this.paddingRight,
12572 			this.height - this.paddingTop - this.paddingBottom);
12573 
12574 		/+
12575 		writeln(l.width, "x", l.height);
12576 		+/
12577 	}
12578 }
12579 +/
12580 
12581 
12582 
12583 
12584 /+
12585 	This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too
12586 +/
12587 
12588 /// Contains the implementation of text editing
12589 abstract class EditableTextWidget : EditableTextWidgetParent {
12590 	this(Widget parent) {
12591 		super(parent);
12592 
12593 		version(custom_widgets)
12594 			setupCustomTextEditing();
12595 	}
12596 
12597 	private bool wordWrapEnabled_;
12598 	void wordWrapEnabled(bool enabled) {
12599 		version(win32_widgets) {
12600 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
12601 		} else version(custom_widgets) {
12602 			wordWrapEnabled_ = enabled;
12603 			version(use_new_text_system)
12604 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
12605 		} else static assert(false);
12606 	}
12607 
12608 	override int minWidth() { return scaleWithDpi(16); }
12609 	override int widthStretchiness() { return 7; }
12610 
12611 	version(use_new_text_system)
12612 	override int maxHeight() { return tdh.maxHeight; }
12613 
12614 	version(use_new_text_system)
12615 	override void focus() { if(tdh) tdh.focus(); else super.focus(); }
12616 
12617 	void selectAll() {
12618 		version(win32_widgets)
12619 			SendMessage(hwnd, EM_SETSEL, 0, -1);
12620 		else version(custom_widgets) {
12621 			version(use_new_text_system)
12622 				tdh.selectAll();
12623 			else
12624 				textLayout.selectAll();
12625 			redraw();
12626 		}
12627 	}
12628 
12629 	version(use_new_text_system)
12630 		TextDisplayHelper tdh;
12631 
12632 	@property string content() {
12633 		version(win32_widgets) {
12634 			wchar[4096] bufferstack;
12635 			wchar[] buffer;
12636 			auto len = GetWindowTextLength(hwnd);
12637 			if(len < bufferstack.length)
12638 				buffer = bufferstack[0 .. len + 1];
12639 			else
12640 				buffer = new wchar[](len + 1);
12641 
12642 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
12643 			if(l >= 0)
12644 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
12645 			else
12646 				return null;
12647 		} else version(custom_widgets) {
12648 			version(use_new_text_system) {
12649 				return textLayout.getTextString();
12650 			} else
12651 				return textLayout.getPlainText();
12652 		} else static assert(false);
12653 	}
12654 	@property void content(string s) {
12655 		version(win32_widgets) {
12656 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
12657 			SetWindowTextW(hwnd, bfr.ptr);
12658 		} else version(custom_widgets) {
12659 			version(use_new_text_system) {
12660 				selectAll();
12661 				textLayout.selection.replaceContent(s);
12662 
12663 				tdh.adjustScrollbarSizes();
12664 				// these don't seem to help
12665 				// tdh.smw.setPosition(0, 0);
12666 				// tdh.scrollForCaret();
12667 
12668 				redraw();
12669 			} else {
12670 				textLayout.clear();
12671 				textLayout.addText(s);
12672 
12673 				{
12674 				// FIXME: it should be able to get this info easier
12675 				auto painter = draw();
12676 				textLayout.redoLayout(painter);
12677 				}
12678 				auto cbb = textLayout.contentBoundingBox();
12679 				setContentSize(cbb.width, cbb.height);
12680 				/*
12681 				textLayout.addText(ForegroundColor.red, s);
12682 				textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/");
12683 				textLayout.addText(" is the best!");
12684 				*/
12685 				redraw();
12686 			}
12687 		}
12688 		else static assert(false);
12689 	}
12690 
12691 	void addText(string txt) {
12692 		version(custom_widgets) {
12693 			version(use_new_text_system) {
12694 				textLayout.appendText(txt);
12695 				tdh.adjustScrollbarSizes();
12696 				redraw();
12697 			} else {
12698 				textLayout.addText(txt);
12699 
12700 				{
12701 				// FIXME: it should be able to get this info easier
12702 				auto painter = draw();
12703 				textLayout.redoLayout(painter);
12704 				}
12705 				auto cbb = textLayout.contentBoundingBox();
12706 				setContentSize(cbb.width, cbb.height);
12707 			}
12708 		} else version(win32_widgets) {
12709 			// get the current selection
12710 			DWORD StartPos, EndPos;
12711 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
12712 
12713 			// move the caret to the end of the text
12714 			int outLength = GetWindowTextLengthW(hwnd);
12715 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
12716 
12717 			// insert the text at the new caret position
12718 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
12719 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
12720 
12721 			// restore the previous selection
12722 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
12723 		} else static assert(0);
12724 	}
12725 
12726 	version(custom_widgets)
12727 	version(trash_text)
12728 	override void paintFrameAndBackground(WidgetPainter painter) {
12729 		this.draw3dFrame(painter, FrameStyle.sunk, Color.white);
12730 	}
12731 
12732 	version(use_new_text_system)
12733 	TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
12734 		return new TextDisplayHelper(textLayout, smw);
12735 	}
12736 
12737 	version(use_new_text_system)
12738 	TextStyle defaultTextStyle() {
12739 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
12740 	}
12741 
12742 	version(use_new_text_system)
12743 	private OperatingSystemFont getUsedFont() {
12744 		auto cs = getComputedStyle();
12745 		auto font = cs.font;
12746 		if(font is null) {
12747 			font = new OperatingSystemFont;
12748 			font.loadDefault();
12749 		}
12750 		return font;
12751 	}
12752 
12753 	version(win32_widgets) { /* will do it with Windows calls in the classes */ }
12754 	else version(custom_widgets) {
12755 		// FIXME
12756 		version(use_new_text_system) {
12757 			TextLayouter textLayout;
12758 
12759 			void setupCustomTextEditing() {
12760 				textLayout = new TextLayouter(defaultTextStyle());
12761 				auto smw = new ScrollMessageWidget(this);
12762 				if(!showingHorizontalScroll)
12763 					smw.horizontalScrollBar.hide();
12764 				if(!showingVerticalScroll)
12765 					smw.verticalScrollBar.hide();
12766 				this.tabStop = false;
12767 				smw.tabStop = false;
12768 				tdh = textDisplayHelperFactory(textLayout, smw);
12769 
12770 				this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
12771 					if(textLayout) {
12772 						if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
12773 							// the dpi change can change the font, so this informs the layouter that it has changed too
12774 							style.font_ = getUsedFont();
12775 
12776 							// arsd.core.writeln(this.parentWindow.win.actualDpi);
12777 						}
12778 					}
12779 				});
12780 			}
12781 
12782 		} else {
12783 
12784 			static if(SimpledisplayTimerAvailable)
12785 				Timer caretTimer;
12786 			etc.TextLayout textLayout;
12787 
12788 			void setupCustomTextEditing() {
12789 				textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4));
12790 				textLayout.selectionXorColor = getComputedStyle().activeListXorColor;
12791 			}
12792 
12793 			override void paint(WidgetPainter painter) {
12794 				if(parentWindow.win.closed) return;
12795 
12796 				textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4);
12797 
12798 				/*
12799 				painter.outlineColor = Color.white;
12800 				painter.fillColor = Color.white;
12801 				painter.drawRectangle(Point(4, 4), contentWidth, contentHeight);
12802 				*/
12803 
12804 				painter.outlineColor = Color.black;
12805 				// painter.drawText(Point(4, 4), content, Point(width - 4, height - 4));
12806 
12807 				textLayout.caretShowingOnScreen = false;
12808 
12809 				textLayout.drawInto(painter, !parentWindow.win.closed && isFocused());
12810 			}
12811 		}
12812 
12813 		static class Style : Widget.Style {
12814 			override FrameStyle borderStyle() {
12815 				return FrameStyle.sunk;
12816 			}
12817 			override MouseCursor cursor() {
12818 				return GenericCursor.Text;
12819 			}
12820 		}
12821 		mixin OverrideStyle!Style;
12822 	}
12823 	else static assert(false);
12824 
12825 	version(trash_text)
12826 	version(custom_widgets)
12827 	override void defaultEventHandler_mousedown(MouseDownEvent ev) {
12828 		super.defaultEventHandler_mousedown(ev);
12829 		if(parentWindow.win.closed) return;
12830 		if(ev.button == MouseButton.left) {
12831 			if(textLayout.selectNone())
12832 				redraw();
12833 			textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY);
12834 			this.focus();
12835 			//this.parentWindow.win.grabInput();
12836 		} else if(ev.button == MouseButton.middle) {
12837 			static if(UsingSimpledisplayX11) {
12838 				getPrimarySelection(parentWindow.win, (in char[] txt) {
12839 					textLayout.insert(txt);
12840 					redraw();
12841 
12842 					auto cbb = textLayout.contentBoundingBox();
12843 					setContentSize(cbb.width, cbb.height);
12844 				});
12845 			}
12846 		}
12847 	}
12848 
12849 	version(trash_text)
12850 	version(custom_widgets)
12851 	override void defaultEventHandler_mouseup(MouseUpEvent ev) {
12852 		//this.parentWindow.win.releaseInputGrab();
12853 		super.defaultEventHandler_mouseup(ev);
12854 	}
12855 
12856 	version(trash_text)
12857 	version(custom_widgets)
12858 	override void defaultEventHandler_mousemove(MouseMoveEvent ev) {
12859 		super.defaultEventHandler_mousemove(ev);
12860 		if(ev.state & ModifierState.leftButtonDown) {
12861 			textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY);
12862 			redraw();
12863 		}
12864 	}
12865 
12866 	version(trash_text)
12867 	version(custom_widgets)
12868 	override void defaultEventHandler_focus(Event ev) {
12869 		super.defaultEventHandler_focus(ev);
12870 		if(parentWindow.win.closed) return;
12871 		auto painter = this.draw();
12872 		textLayout.drawCaret(painter);
12873 
12874 		static if(SimpledisplayTimerAvailable)
12875 		if(caretTimer) {
12876 			caretTimer.destroy();
12877 			caretTimer = null;
12878 		}
12879 
12880 		bool blinkingCaret = true;
12881 		static if(UsingSimpledisplayX11)
12882 			if(!Image.impl.xshmAvailable)
12883 				blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink
12884 
12885 		if(blinkingCaret)
12886 		static if(SimpledisplayTimerAvailable)
12887 		caretTimer = new Timer(500, {
12888 			if(parentWindow.win.closed) {
12889 				caretTimer.destroy();
12890 				return;
12891 			}
12892 			if(isFocused()) {
12893 				auto painter = this.draw();
12894 				textLayout.drawCaret(painter);
12895 			} else if(textLayout.caretShowingOnScreen) {
12896 				auto painter = this.draw();
12897 				textLayout.eraseCaret(painter);
12898 			}
12899 		});
12900 	}
12901 
12902 	version(trash_text) {
12903 		private string lastContentBlur;
12904 
12905 		override void defaultEventHandler_blur(Event ev) {
12906 			super.defaultEventHandler_blur(ev);
12907 			if(parentWindow.win.closed) return;
12908 			version(custom_widgets) {
12909 				auto painter = this.draw();
12910 				textLayout.eraseCaret(painter);
12911 				static if(SimpledisplayTimerAvailable)
12912 				if(caretTimer) {
12913 					caretTimer.destroy();
12914 					caretTimer = null;
12915 				}
12916 			}
12917 
12918 			if(this.content != lastContentBlur) {
12919 				auto evt = new ChangeEvent!string(this, &this.content);
12920 				evt.dispatch();
12921 				lastContentBlur = this.content;
12922 			}
12923 		}
12924 	}
12925 
12926 	version(win32_widgets) {
12927 		private string lastContentBlur;
12928 
12929 		override void defaultEventHandler_blur(Event ev) {
12930 			super.defaultEventHandler_blur(ev);
12931 
12932 			if(this.content != lastContentBlur) {
12933 				auto evt = new ChangeEvent!string(this, &this.content);
12934 				evt.dispatch();
12935 				lastContentBlur = this.content;
12936 			}
12937 		}
12938 	}
12939 
12940 
12941 	version(trash_text)
12942 	version(custom_widgets)
12943 	override void defaultEventHandler_char(CharEvent ev) {
12944 		super.defaultEventHandler_char(ev);
12945 		textLayout.insert(ev.character);
12946 		redraw();
12947 
12948 		// FIXME: too inefficient
12949 		auto cbb = textLayout.contentBoundingBox();
12950 		setContentSize(cbb.width, cbb.height);
12951 	}
12952 	version(trash_text)
12953 	version(custom_widgets)
12954 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
12955 		//super.defaultEventHandler_keydown(ev);
12956 		switch(ev.key) {
12957 			case Key.Delete:
12958 				textLayout.delete_();
12959 				redraw();
12960 			break;
12961 			case Key.Left:
12962 				textLayout.moveLeft();
12963 				redraw();
12964 			break;
12965 			case Key.Right:
12966 				textLayout.moveRight();
12967 				redraw();
12968 			break;
12969 			case Key.Up:
12970 				textLayout.moveUp();
12971 				redraw();
12972 			break;
12973 			case Key.Down:
12974 				textLayout.moveDown();
12975 				redraw();
12976 			break;
12977 			case Key.Home:
12978 				textLayout.moveHome();
12979 				redraw();
12980 			break;
12981 			case Key.End:
12982 				textLayout.moveEnd();
12983 				redraw();
12984 			break;
12985 			case Key.PageUp:
12986 				foreach(i; 0 .. 32)
12987 				textLayout.moveUp();
12988 				redraw();
12989 			break;
12990 			case Key.PageDown:
12991 				foreach(i; 0 .. 32)
12992 				textLayout.moveDown();
12993 				redraw();
12994 			break;
12995 
12996 			default:
12997 				 {} // intentionally blank, let "char" handle it
12998 		}
12999 		/*
13000 		if(ev.key == Key.Backspace) {
13001 			textLayout.backspace();
13002 			redraw();
13003 		}
13004 		*/
13005 		ensureVisibleInScroll(textLayout.caretBoundingBox());
13006 	}
13007 
13008 	version(use_new_text_system) {
13009 		bool showingVerticalScroll() { return true; }
13010 		bool showingHorizontalScroll() { return true; }
13011 	}
13012 }
13013 
13014 ///
13015 class LineEdit : EditableTextWidget {
13016 	// FIXME: hack
13017 	version(custom_widgets) {
13018 	override bool showingVerticalScroll() { return false; }
13019 	override bool showingHorizontalScroll() { return false; }
13020 	}
13021 
13022 	override int flexBasisWidth() { return 250; }
13023 
13024 	///
13025 	this(Widget parent) {
13026 		super(parent);
13027 		version(win32_widgets) {
13028 			createWin32Window(this, "edit"w, "",
13029 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13030 		} else version(custom_widgets) {
13031 			version(trash_text) {
13032 				setupCustomTextEditing();
13033 				addEventListener(delegate(CharEvent ev) {
13034 					if(ev.character == '\n')
13035 						ev.preventDefault();
13036 				});
13037 			}
13038 		} else static assert(false);
13039 	}
13040 
13041 	version(use_new_text_system)
13042 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13043 		auto tdh = new TextDisplayHelper(textLayout, smw);
13044 		tdh.singleLine = true;
13045 		return tdh;
13046 	}
13047 
13048 	version(win32_widgets) {
13049 		mixin Padding!q{2};
13050 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13051 		override int maxHeight() { return minHeight; }
13052 	}
13053 
13054 	/+
13055 	@property void passwordMode(bool p) {
13056 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
13057 	}
13058 	+/
13059 }
13060 
13061 /++
13062 	A [LineEdit] that displays `*` in place of the actual characters.
13063 
13064 	Alas, Windows requires the window to be created differently to use this style,
13065 	so it had to be a new class instead of a toggle on and off on an existing object.
13066 
13067 	FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now.
13068 
13069 	History:
13070 		Added January 24, 2021
13071 +/
13072 class PasswordEdit : EditableTextWidget {
13073 	version(custom_widgets) {
13074 	override bool showingVerticalScroll() { return false; }
13075 	override bool showingHorizontalScroll() { return false; }
13076 	}
13077 
13078 	override int flexBasisWidth() { return 250; }
13079 
13080 	version(use_new_text_system)
13081 	override TextStyle defaultTextStyle() {
13082 		auto cs = getComputedStyle();
13083 
13084 		auto osf = new class OperatingSystemFont {
13085 			this() {
13086 				super(cs.font);
13087 			}
13088 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
13089 				int count = 0;
13090 				foreach(dchar ch; text)
13091 					count++;
13092 				return count * super.stringWidth("*", window);
13093 			}
13094 		};
13095 
13096 		return new TextDisplayHelper.MyTextStyle(osf);
13097 	}
13098 
13099 	version(use_new_text_system)
13100 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13101 		static class TDH : TextDisplayHelper {
13102 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
13103 				singleLine = true;
13104 				super(textLayout, smw);
13105 			}
13106 
13107 			override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
13108 				char[256] buffer = void;
13109 				int bufferLength = 0;
13110 				foreach(dchar ch; text)
13111 					buffer[bufferLength++] = '*';
13112 				painter.drawText(upperLeft, buffer[0..bufferLength]);
13113 			}
13114 		}
13115 
13116 		return new TDH(textLayout, smw);
13117 	}
13118 
13119 	///
13120 	this(Widget parent) {
13121 		super(parent);
13122 		version(win32_widgets) {
13123 			createWin32Window(this, "edit"w, "",
13124 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13125 		} else version(custom_widgets) {
13126 			version(trash_text)
13127 			setupCustomTextEditing();
13128 			addEventListener(delegate(CharEvent ev) {
13129 				if(ev.character == '\n')
13130 					ev.preventDefault();
13131 			});
13132 		} else static assert(false);
13133 	}
13134 	version(win32_widgets) {
13135 		mixin Padding!q{2};
13136 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13137 		override int maxHeight() { return minHeight; }
13138 	}
13139 }
13140 
13141 ///
13142 class TextEdit : EditableTextWidget {
13143 	///
13144 	this(Widget parent) {
13145 		super(parent);
13146 		version(win32_widgets) {
13147 			createWin32Window(this, "edit"w, "",
13148 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
13149 		} else version(custom_widgets) {
13150 			version(trash_text)
13151 			setupCustomTextEditing();
13152 		} else static assert(false);
13153 	}
13154 	override int maxHeight() { return int.max; }
13155 	override int heightStretchiness() { return 7; }
13156 
13157 	override int flexBasisWidth() { return 250; }
13158 	override int flexBasisHeight() { return 25; }
13159 }
13160 
13161 
13162 /+
13163 /++
13164 
13165 +/
13166 version(none)
13167 class RichTextDisplay : Widget {
13168 	@property void content(string c) {}
13169 	void appendContent(string c) {}
13170 }
13171 +/
13172 
13173 /++
13174 	A read-only text display
13175 
13176 	History:
13177 		Added October 31, 2023 (dub v11.3)
13178 +/
13179 class TextDisplay : EditableTextWidget {
13180 	this(string text, Widget parent) {
13181 		super(parent);
13182 		this.content = text;
13183 	}
13184 
13185 	override int maxHeight() { return int.max; }
13186 	override int minHeight() { return 50; }
13187 	override int heightStretchiness() { return 7; }
13188 
13189 	override int flexBasisWidth() { return 250; }
13190 	override int flexBasisHeight() { return 50; }
13191 
13192 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13193 		return new MyTextDisplayHelper(textLayout, smw);
13194 	}
13195 
13196 	override void registerMovement() {
13197 		super.registerMovement();
13198 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
13199 	}
13200 
13201 	static class MyTextDisplayHelper : TextDisplayHelper {
13202 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
13203 			smw.verticalScrollBar.hide();
13204 			smw.horizontalScrollBar.hide();
13205 			super(textLayout, smw);
13206 			this.readonly = true;
13207 		}
13208 
13209 		class Style : Widget.Style {
13210 			// just want the generic look for these
13211 		}
13212 
13213 		mixin OverrideStyle!Style;
13214 	}
13215 }
13216 
13217 ///
13218 class MessageBox : Window {
13219 	private string message;
13220 	MessageBoxButton buttonPressed = MessageBoxButton.None;
13221 	///
13222 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
13223 		super(300, 100);
13224 
13225 		assert(buttons.length);
13226 		assert(buttons.length ==  buttonIds.length);
13227 
13228 		this.message = message;
13229 
13230 		auto label = new TextDisplay(message, this);
13231 
13232 		auto hl = new HorizontalLayout(this);
13233 		auto spacer = new HorizontalSpacer(hl); // to right align
13234 
13235 		foreach(idx, buttonText; buttons) {
13236 			auto button = new CommandButton(buttonText, hl);
13237 
13238 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
13239 				this.buttonPressed = buttonIds[idx];
13240 				win.close();
13241 			}; })(idx));
13242 
13243 			if(idx == 0)
13244 				button.focus();
13245 		}
13246 
13247 		if(buttons.length == 1)
13248 			auto spacer2 = new HorizontalSpacer(hl); // to center it
13249 
13250 		win.resize(scaleWithDpi(300), this.minHeight());
13251 
13252 		win.show();
13253 		redraw();
13254 	}
13255 
13256 	mixin Padding!q{16};
13257 }
13258 
13259 ///
13260 enum MessageBoxStyle {
13261 	OK, ///
13262 	OKCancel, ///
13263 	RetryCancel, ///
13264 	YesNo, ///
13265 	YesNoCancel, ///
13266 	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.
13267 }
13268 
13269 ///
13270 enum MessageBoxIcon {
13271 	None, ///
13272 	Info, ///
13273 	Warning, ///
13274 	Error ///
13275 }
13276 
13277 /// Identifies the button the user pressed on a message box.
13278 enum MessageBoxButton {
13279 	None, /// The user closed the message box without clicking any of the buttons.
13280 	OK, ///
13281 	Cancel, ///
13282 	Retry, ///
13283 	Yes, ///
13284 	No, ///
13285 	Continue ///
13286 }
13287 
13288 
13289 /++
13290 	Displays a modal message box, blocking until the user dismisses it.
13291 
13292 	Returns: the button pressed.
13293 +/
13294 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13295 	version(win32_widgets) {
13296 		WCharzBuffer t = WCharzBuffer(title);
13297 		WCharzBuffer m = WCharzBuffer(message);
13298 		UINT type;
13299 		with(MessageBoxStyle)
13300 		final switch(style) {
13301 			case OK: type |= MB_OK; break;
13302 			case OKCancel: type |= MB_OKCANCEL; break;
13303 			case RetryCancel: type |= MB_RETRYCANCEL; break;
13304 			case YesNo: type |= MB_YESNO; break;
13305 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
13306 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
13307 		}
13308 		with(MessageBoxIcon)
13309 		final switch(icon) {
13310 			case None: break;
13311 			case Info: type |= MB_ICONINFORMATION; break;
13312 			case Warning: type |= MB_ICONWARNING; break;
13313 			case Error: type |= MB_ICONERROR; break;
13314 		}
13315 		switch(MessageBoxW(null, m.ptr, t.ptr, type)) {
13316 			case IDOK: return MessageBoxButton.OK;
13317 			case IDCANCEL: return MessageBoxButton.Cancel;
13318 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
13319 			case IDYES: return MessageBoxButton.Yes;
13320 			case IDNO: return MessageBoxButton.No;
13321 			case IDCONTINUE: return MessageBoxButton.Continue;
13322 			default: return MessageBoxButton.None;
13323 		}
13324 	} else {
13325 		string[] buttons;
13326 		MessageBoxButton[] buttonIds;
13327 		with(MessageBoxStyle)
13328 		final switch(style) {
13329 			case OK:
13330 				buttons = ["OK"];
13331 				buttonIds = [MessageBoxButton.OK];
13332 			break;
13333 			case OKCancel:
13334 				buttons = ["OK", "Cancel"];
13335 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
13336 			break;
13337 			case RetryCancel:
13338 				buttons = ["Retry", "Cancel"];
13339 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
13340 			break;
13341 			case YesNo:
13342 				buttons = ["Yes", "No"];
13343 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
13344 			break;
13345 			case YesNoCancel:
13346 				buttons = ["Yes", "No", "Cancel"];
13347 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
13348 			break;
13349 			case RetryCancelContinue:
13350 				buttons = ["Try Again", "Cancel", "Continue"];
13351 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
13352 			break;
13353 		}
13354 		auto mb = new MessageBox(message, buttons, buttonIds);
13355 		EventLoop el = EventLoop.get;
13356 		el.run(() { return !mb.win.closed; });
13357 		return mb.buttonPressed;
13358 	}
13359 }
13360 
13361 /// ditto
13362 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13363 	return messageBox(null, message, style, icon);
13364 }
13365 
13366 
13367 
13368 ///
13369 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
13370 
13371 /++
13372 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
13373 
13374 	History:
13375 		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.
13376 +/
13377 struct EventListener {
13378 	private Widget widget;
13379 	private string event;
13380 	private EventHandler handler;
13381 	private bool useCapture;
13382 
13383 	///
13384 	void disconnect() {
13385 		widget.removeEventListener(this);
13386 	}
13387 }
13388 
13389 /++
13390 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
13391 
13392 	Now, I recommend you use a statically typed event object instead.
13393 
13394 	See_Also: [Event]
13395 +/
13396 enum EventType : string {
13397 	click = "click", ///
13398 
13399 	mouseenter = "mouseenter", ///
13400 	mouseleave = "mouseleave", ///
13401 	mousein = "mousein", ///
13402 	mouseout = "mouseout", ///
13403 	mouseup = "mouseup", ///
13404 	mousedown = "mousedown", ///
13405 	mousemove = "mousemove", ///
13406 
13407 	keydown = "keydown", ///
13408 	keyup = "keyup", ///
13409 	char_ = "char", ///
13410 
13411 	focus = "focus", ///
13412 	blur = "blur", ///
13413 
13414 	triggered = "triggered", ///
13415 
13416 	change = "change", ///
13417 }
13418 
13419 /++
13420 	Represents an event that is currently being processed.
13421 
13422 
13423 	Minigui's event model is based on the web browser. An event has a name, a target,
13424 	and an associated data object. It starts from the window and works its way down through
13425 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
13426 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
13427 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
13428 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
13429 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
13430 	whenever propagation is done, not only if it gets to the end of the chain).
13431 
13432 	This model has several nice points:
13433 
13434 	$(LIST
13435 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
13436 		  with event handlers set, then add/remove children as much as you want without needing
13437 		  to manage the event handlers on them - the parent alone can manage everything.
13438 
13439 		* It is easy to create new custom events in your application.
13440 
13441 		* It is familiar to many web developers.
13442 	)
13443 
13444 	There's a few downsides though:
13445 
13446 	$(LIST
13447 		* There's not a lot of type safety.
13448 
13449 		* You don't get a static list of what events a widget can emit.
13450 
13451 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
13452 		  the central delegation benefit is it can be lead to debugging of action at a distance.
13453 	)
13454 
13455 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
13456 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
13457 	to simply use a D object type which provides a static interface as well as a built-in event name.
13458 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
13459 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
13460 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
13461 	to having a little more help from the D compiler and documentation generator.
13462 
13463 	Your code would change like this:
13464 
13465 	---
13466 	// old
13467 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
13468 
13469 	// new
13470 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
13471 	---
13472 
13473 	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.
13474 
13475 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
13476 
13477 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
13478 
13479 	Thus the family of functions are:
13480 
13481 	[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.
13482 
13483 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
13484 
13485 	[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.
13486 
13487 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
13488 
13489 	---
13490 	class MyCheckbox : Widget {
13491 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
13492 		/// It is NOT actually required but should be used whenever possible.
13493 		mixin Emits!(ChangeEvent!bool);
13494 
13495 		this(Widget parent) {
13496 			super(parent);
13497 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
13498 		}
13499 
13500 		private bool _checked;
13501 		@property bool checked() { return _checked; }
13502 		@property void checked(bool set) {
13503 			_checked = set;
13504 			emit!(ChangeEvent!bool)(&checked);
13505 		}
13506 	}
13507 	---
13508 
13509 	## Creating Your Own Events
13510 
13511 	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.
13512 
13513 	---
13514 	class MyEvent : Event {
13515 		this(Widget target) { super(EventString, target); }
13516 		mixin Register; // adds EventString and other reflection information
13517 	}
13518 	---
13519 
13520 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
13521 
13522 	History:
13523 		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.
13524 
13525 		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.
13526 +/
13527 /+
13528 
13529 	## General Conventions
13530 
13531 	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.
13532 
13533 
13534 	## Qt-style signals and slots
13535 
13536 	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.
13537 
13538 	The intention is for events to be used when
13539 
13540 	---
13541 	class Demo : Widget {
13542 		this() {
13543 			myPropertyChanged = Signal!int(this);
13544 		}
13545 		@property myProperty(int v) {
13546 			myPropertyChanged.emit(v);
13547 		}
13548 
13549 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
13550 		// but it can just genuinely not care about `this` since that's not really passed.
13551 	}
13552 
13553 	class Foo : Widget {
13554 		// the slot uda is not necessary, but it helps the script and ui builder find it.
13555 		@slot void setValue(int v) { ... }
13556 	}
13557 
13558 	demo.myPropertyChanged.connect(&foo.setValue);
13559 	---
13560 
13561 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
13562 
13563 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
13564 
13565 	class StringChangeEvent : ChangeEvent, Signal!string {
13566 		mixin SignalImpl
13567 	}
13568 
13569 +/
13570 class Event : ReflectableProperties {
13571 	/// Creates an event without populating any members and without sending it. See [dispatch]
13572 	this(string eventName, Widget emittedBy) {
13573 		this.eventName = eventName;
13574 		this.srcElement = emittedBy;
13575 	}
13576 
13577 
13578 	/// Implementations for the [ReflectableProperties] interface/
13579 	void getPropertiesList(scope void delegate(string name) sink) const {}
13580 	/// ditto
13581 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
13582 	/// ditto
13583 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
13584 		return SetPropertyResult.notPermitted;
13585 	}
13586 
13587 
13588 	/+
13589 	/++
13590 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
13591 
13592 		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.
13593 	+/
13594 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
13595 		if(value.length == 0) {
13596 			finalSink(memberName, `""`);
13597 			return;
13598 		}
13599 
13600 		char[1024] bufferBacking;
13601 		char[] buffer = bufferBacking;
13602 		int bufferPosition;
13603 
13604 		void sink(char ch) {
13605 			if(bufferPosition >= buffer.length)
13606 				buffer.length = buffer.length + 1024;
13607 			buffer[bufferPosition++] = ch;
13608 		}
13609 
13610 		sink('"');
13611 
13612 		foreach(ch; value) {
13613 			switch(ch) {
13614 				case '\\':
13615 					sink('\\'); sink('\\');
13616 				break;
13617 				case '"':
13618 					sink('\\'); sink('"');
13619 				break;
13620 				case '\n':
13621 					sink('\\'); sink('n');
13622 				break;
13623 				case '\r':
13624 					sink('\\'); sink('r');
13625 				break;
13626 				case '\t':
13627 					sink('\\'); sink('t');
13628 				break;
13629 				default:
13630 					sink(ch);
13631 			}
13632 		}
13633 
13634 		sink('"');
13635 
13636 		finalSink(memberName, buffer[0 .. bufferPosition]);
13637 	}
13638 	+/
13639 
13640 	/+
13641 	enum EventInitiator {
13642 		system,
13643 		minigui,
13644 		user
13645 	}
13646 
13647 	immutable EventInitiator; initiatedBy;
13648 	+/
13649 
13650 	/++
13651 		Events should generally follow the propagation model, but there's some exceptions
13652 		to that rule. If so, they should override this to return false. In that case, only
13653 		bubbling event handlers on the target itself and capturing event handlers on the containing
13654 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
13655 		capture -> target -> bubble process.)
13656 
13657 		History:
13658 			Added May 12, 2021
13659 	+/
13660 	bool propagates() const pure nothrow @nogc @safe {
13661 		return true;
13662 	}
13663 
13664 	/++
13665 		hints as to whether preventDefault will actually do anything. not entirely reliable.
13666 
13667 		History:
13668 			Added May 14, 2021
13669 	+/
13670 	bool cancelable() const pure nothrow @nogc @safe {
13671 		return true;
13672 	}
13673 
13674 	/++
13675 		You can mix this into child class to register some boilerplate. It includes the `EventString`
13676 		member, a constructor, and implementations of the dynamic get data interfaces.
13677 
13678 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
13679 
13680 
13681 		You can override the default EventString by simply providing your own in the form of
13682 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
13683 		which provides some namespace protection against conflicts in other libraries while still being fairly
13684 		easy to use.
13685 
13686 		If you provide your own constructor, it will override the default constructor provided here. A constructor
13687 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
13688 		first argument to your constructor.
13689 
13690 		History:
13691 			Added May 13, 2021.
13692 	+/
13693 	protected static mixin template Register() {
13694 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
13695 		this(Widget target) { super(EventString, target); }
13696 
13697 		mixin ReflectableProperties.RegisterGetters;
13698 	}
13699 
13700 	/++
13701 		This is the widget that emitted the event.
13702 
13703 
13704 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
13705 
13706 		History:
13707 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
13708 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
13709 			so I don't intend to remove these aliases.
13710 	+/
13711 	Widget source;
13712 	/// ditto
13713 	alias source target;
13714 	/// ditto
13715 	alias source srcElement;
13716 
13717 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
13718 
13719 	/// Prevents the default event handler (if there is one) from being called
13720 	void preventDefault() {
13721 		lastDefaultPrevented = true;
13722 		defaultPrevented = true;
13723 	}
13724 
13725 	/// Stops the event propagation immediately.
13726 	void stopPropagation() {
13727 		propagationStopped = true;
13728 	}
13729 
13730 	private bool defaultPrevented;
13731 	private bool propagationStopped;
13732 	private string eventName;
13733 
13734 	private bool isBubbling;
13735 
13736 	/// 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.
13737 	protected void adjustScrolling() { }
13738 	/// ditto
13739 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
13740 
13741 	/++
13742 		this sends it only to the target. If you want propagation, use dispatch() instead.
13743 
13744 		This should be made private!!!
13745 
13746 	+/
13747 	void sendDirectly() {
13748 		if(srcElement is null)
13749 			return;
13750 
13751 		// 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.
13752 
13753 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
13754 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
13755 
13756 		adjustScrolling();
13757 
13758 		if(auto e = target.parentWindow) {
13759 			if(auto handlers = "*" in e.capturingEventHandlers)
13760 			foreach(handler; *handlers)
13761 				if(handler) handler(e, this);
13762 			if(auto handlers = eventName in e.capturingEventHandlers)
13763 			foreach(handler; *handlers)
13764 				if(handler) handler(e, this);
13765 		}
13766 
13767 		auto e = srcElement;
13768 
13769 		if(auto handlers = eventName in e.bubblingEventHandlers)
13770 		foreach(handler; *handlers)
13771 			if(handler) handler(e, this);
13772 
13773 		if(auto handlers = "*" in e.bubblingEventHandlers)
13774 		foreach(handler; *handlers)
13775 			if(handler) handler(e, this);
13776 
13777 		// there's never a default for a catch-all event
13778 		if(!defaultPrevented)
13779 			if(eventName in e.defaultEventHandlers)
13780 				e.defaultEventHandlers[eventName](e, this);
13781 	}
13782 
13783 	/// this dispatches the element using the capture -> target -> bubble process
13784 	void dispatch() {
13785 		if(srcElement is null)
13786 			return;
13787 
13788 		if(!propagates) {
13789 			sendDirectly;
13790 			return;
13791 		}
13792 
13793 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
13794 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
13795 
13796 		adjustScrolling();
13797 		// first capture, then bubble
13798 
13799 		Widget[] chain;
13800 		Widget curr = srcElement;
13801 		while(curr) {
13802 			auto l = curr;
13803 			chain ~= l;
13804 			curr = curr.parent;
13805 		}
13806 
13807 		isBubbling = false;
13808 
13809 		foreach_reverse(e; chain) {
13810 			if(auto handlers = "*" in e.capturingEventHandlers)
13811 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13812 
13813 			if(propagationStopped)
13814 				break;
13815 
13816 			if(auto handlers = eventName in e.capturingEventHandlers)
13817 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13818 
13819 			// the default on capture should really be to always do nothing
13820 
13821 			//if(!defaultPrevented)
13822 			//	if(eventName in e.defaultEventHandlers)
13823 			//		e.defaultEventHandlers[eventName](e.element, this);
13824 
13825 			if(propagationStopped)
13826 				break;
13827 		}
13828 
13829 		int adjustX;
13830 		int adjustY;
13831 
13832 		isBubbling = true;
13833 		if(!propagationStopped)
13834 		foreach(e; chain) {
13835 			if(auto handlers = eventName in e.bubblingEventHandlers)
13836 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13837 
13838 			if(propagationStopped)
13839 				break;
13840 
13841 			if(auto handlers = "*" in e.bubblingEventHandlers)
13842 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13843 
13844 			if(propagationStopped)
13845 				break;
13846 
13847 			if(e.encapsulatedChildren()) {
13848 				adjustClientCoordinates(adjustX, adjustY);
13849 				target = e;
13850 			} else {
13851 				adjustX += e.x;
13852 				adjustY += e.y;
13853 			}
13854 		}
13855 
13856 		if(!defaultPrevented)
13857 		foreach(e; chain) {
13858 			if(eventName in e.defaultEventHandlers)
13859 				e.defaultEventHandlers[eventName](e, this);
13860 		}
13861 	}
13862 
13863 
13864 	/* old compatibility things */
13865 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
13866 	final @property {
13867 		Key key() { return (cast(KeyEventBase) this).key; }
13868 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
13869 
13870 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
13871 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
13872 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
13873 	}
13874 
13875 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
13876 	final @property {
13877 		int clientX() { return (cast(MouseEventBase) this).clientX; }
13878 		int clientY() { return (cast(MouseEventBase) this).clientY; }
13879 
13880 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
13881 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
13882 
13883 		int button() { return (cast(MouseEventBase) this).button; }
13884 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
13885 	}
13886 
13887 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
13888 	final @property {
13889 		int state() {
13890 			if(auto meb = cast(MouseEventBase) this)
13891 				return meb.state;
13892 			if(auto keb = cast(KeyEventBase) this)
13893 				return keb.state;
13894 			assert(0);
13895 		}
13896 	}
13897 
13898 	deprecated("Use a CharEvent instead of Event in your handler going forward")
13899 	final @property {
13900 		dchar character() {
13901 			if(auto ce = cast(CharEvent) this)
13902 				return ce.character;
13903 			return dchar.init;
13904 		}
13905 	}
13906 
13907 	// for change events
13908 	@property {
13909 		///
13910 		int intValue() { return 0; }
13911 		///
13912 		string stringValue() { return null; }
13913 	}
13914 }
13915 
13916 /++
13917 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
13918 
13919 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
13920 	dynamic and custom events, but the static list helps ensure you get them right.
13921 
13922 	If this is declared, you can use [Widget.emit] to send the event.
13923 
13924 	All events work the same way though, following the capture->widget->bubble model described under [Event].
13925 
13926 	History:
13927 		Added May 4, 2021
13928 +/
13929 mixin template Emits(EventType) {
13930 	import arsd.minigui : EventString;
13931 	static if(is(EventType : Event) && !is(EventType == Event))
13932 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
13933 	else
13934 		static assert(0, "You can only emit subclasses of Event");
13935 }
13936 
13937 /// ditto
13938 mixin template Emits(string eventString) {
13939 	mixin("private Event[0] emits_" ~ eventString ~";");
13940 }
13941 
13942 /*
13943 class SignalEvent(string name) : Event {
13944 
13945 }
13946 */
13947 
13948 /++
13949 	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".
13950 
13951 
13952 	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.
13953 
13954 	History:
13955 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
13956 +/
13957 class CommandEvent : Event {
13958 	enum EventString = "command";
13959 	this(Widget source, string CommandString = EventString) {
13960 		super(CommandString, source);
13961 	}
13962 }
13963 
13964 /++
13965 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
13966 +/
13967 class CommandEventWithArgs(Args...) : CommandEvent {
13968 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
13969 	Args args;
13970 }
13971 
13972 /++
13973 	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.
13974 
13975 	See [CommandEvent] for more information.
13976 
13977 	Returns:
13978 		The [EventListener] you can use to remove the handler.
13979 +/
13980 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
13981 	return w.addEventListener(CommandString, (Event ev) {
13982 		if(ev.target is w)
13983 			return; // it does not consume its own commands!
13984 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
13985 			handler(cev.args);
13986 			ev.stopPropagation();
13987 		}
13988 	});
13989 }
13990 
13991 /++
13992 	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.
13993 +/
13994 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
13995 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
13996 	event.dispatch();
13997 }
13998 
13999 class ResizeEvent : Event {
14000 	enum EventString = "resize";
14001 
14002 	this(Widget target) { super(EventString, target); }
14003 
14004 	override bool propagates() const { return false; }
14005 }
14006 
14007 /++
14008 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
14009 
14010 	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.
14011 
14012 	History:
14013 		Added June 21, 2021 (dub v10.1)
14014 +/
14015 class ClosingEvent : Event {
14016 	enum EventString = "closing";
14017 
14018 	this(Widget target) { super(EventString, target); }
14019 
14020 	override bool propagates() const { return false; }
14021 	override bool cancelable() const { return true; }
14022 }
14023 
14024 /// ditto
14025 class ClosedEvent : Event {
14026 	enum EventString = "closed";
14027 
14028 	this(Widget target) { super(EventString, target); }
14029 
14030 	override bool propagates() const { return false; }
14031 	override bool cancelable() const { return false; }
14032 }
14033 
14034 ///
14035 class BlurEvent : Event {
14036 	enum EventString = "blur";
14037 
14038 	// FIXME: related target?
14039 	this(Widget target) { super(EventString, target); }
14040 
14041 	override bool propagates() const { return false; }
14042 }
14043 
14044 ///
14045 class FocusEvent : Event {
14046 	enum EventString = "focus";
14047 
14048 	// FIXME: related target?
14049 	this(Widget target) { super(EventString, target); }
14050 
14051 	override bool propagates() const { return false; }
14052 }
14053 
14054 /++
14055 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
14056 
14057 	History:
14058 		Added July 3, 2021
14059 +/
14060 class FocusInEvent : Event {
14061 	enum EventString = "focusin";
14062 
14063 	// FIXME: related target?
14064 	this(Widget target) { super(EventString, target); }
14065 
14066 	override bool cancelable() const { return false; }
14067 }
14068 
14069 /// ditto
14070 class FocusOutEvent : Event {
14071 	enum EventString = "focusout";
14072 
14073 	// FIXME: related target?
14074 	this(Widget target) { super(EventString, target); }
14075 
14076 	override bool cancelable() const { return false; }
14077 }
14078 
14079 ///
14080 class ScrollEvent : Event {
14081 	enum EventString = "scroll";
14082 	this(Widget target) { super(EventString, target); }
14083 
14084 	override bool cancelable() const { return false; }
14085 }
14086 
14087 /++
14088 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
14089 
14090 	History:
14091 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
14092 +/
14093 class CharEvent : Event {
14094 	enum EventString = "char";
14095 	this(Widget target, dchar ch) {
14096 		character = ch;
14097 		super(EventString, target);
14098 	}
14099 
14100 	immutable dchar character;
14101 }
14102 
14103 /++
14104 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
14105 +/
14106 abstract class ChangeEventBase : Event {
14107 	enum EventString = "change";
14108 	this(Widget target) {
14109 		super(EventString, target);
14110 	}
14111 
14112 	/+
14113 		// idk where or how exactly i want to do this.
14114 		// i might come back to it later.
14115 
14116 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
14117 	// this way the source doesn't get too confused (think of a nested scroll widget)
14118 	//
14119 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
14120 	// then you consume that command and change you scroll x position to whatever. then you do
14121 	// some kind of change event that is broadcast back to the children and any horizontal scroll
14122 	// listeners are now able to update, without having an explicit connection between them.
14123 	void broadcastToChildren(string fieldName) {
14124 
14125 	}
14126 	+/
14127 }
14128 
14129 /++
14130 	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.
14131 
14132 
14133 	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).
14134 
14135 	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);`
14136 
14137 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
14138 
14139 	History:
14140 		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.
14141 +/
14142 class ChangeEvent(T) : ChangeEventBase {
14143 	this(Widget target, T delegate() getNewValue) {
14144 		assert(getNewValue !is null);
14145 		this.getNewValue = getNewValue;
14146 		super(target);
14147 	}
14148 
14149 	private T delegate() getNewValue;
14150 
14151 	/++
14152 		Gets the new value that just changed.
14153 	+/
14154 	@property T value() {
14155 		return getNewValue();
14156 	}
14157 
14158 	/// compatibility method for old generic Events
14159 	static if(is(immutable T == immutable int))
14160 		override int intValue() { return value; }
14161 	/// ditto
14162 	static if(is(immutable T == immutable string))
14163 		override string stringValue() { return value; }
14164 }
14165 
14166 /++
14167 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
14168 
14169 
14170 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14171 
14172 	History:
14173 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14174 +/
14175 abstract class KeyEventBase : Event {
14176 	this(string name, Widget target) {
14177 		super(name, target);
14178 	}
14179 
14180 	// for key events
14181 	Key key; ///
14182 
14183 	KeyEvent originalKeyEvent;
14184 
14185 	/++
14186 		Indicates the current state of the given keyboard modifier keys.
14187 
14188 		History:
14189 			Added to events on April 15, 2020.
14190 	+/
14191 	bool ctrlKey;
14192 
14193 	/// ditto
14194 	bool altKey;
14195 
14196 	/// ditto
14197 	bool shiftKey;
14198 
14199 	/++
14200 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
14201 
14202 		See [arsd.simpledisplay.ModifierState] for other possible flags.
14203 	+/
14204 	int state;
14205 
14206 	mixin Register;
14207 }
14208 
14209 /++
14210 	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].
14211 
14212 
14213 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14214 
14215 	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.
14216 
14217 	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.
14218 
14219 	See_Also: [KeyUpEvent], [CharEvent]
14220 
14221 	History:
14222 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
14223 +/
14224 class KeyDownEvent : KeyEventBase {
14225 	enum EventString = "keydown";
14226 	this(Widget target) { super(EventString, target); }
14227 }
14228 
14229 /++
14230 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
14231 
14232 
14233 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14234 
14235 	See_Also: [KeyDownEvent], [CharEvent]
14236 
14237 	History:
14238 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
14239 +/
14240 class KeyUpEvent : KeyEventBase {
14241 	enum EventString = "keyup";
14242 	this(Widget target) { super(EventString, target); }
14243 }
14244 
14245 /++
14246 	Contains shared properties for various mouse events;
14247 
14248 
14249 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14250 
14251 	History:
14252 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14253 +/
14254 abstract class MouseEventBase : Event {
14255 	this(string name, Widget target) {
14256 		super(name, target);
14257 	}
14258 
14259 	// for mouse events
14260 	int clientX; /// The mouse event location relative to the target widget
14261 	int clientY; /// ditto
14262 
14263 	int viewportX; /// The mouse event location relative to the window origin
14264 	int viewportY; /// ditto
14265 
14266 	int button; /// See: [MouseEvent.button]
14267 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
14268 
14269 	/++
14270 		Indicates the current state of the given keyboard modifier keys.
14271 
14272 		History:
14273 			Added to mouse events on September 28, 2010.
14274 	+/
14275 	bool ctrlKey;
14276 
14277 	/// ditto
14278 	bool altKey;
14279 
14280 	/// ditto
14281 	bool shiftKey;
14282 
14283 
14284 
14285 	int state; ///
14286 
14287 	/++
14288 		for consistent names with key event.
14289 
14290 		History:
14291 			Added September 28, 2021 (dub v10.3)
14292 	+/
14293 	alias modifierState = state;
14294 
14295 	/++
14296 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
14297 
14298 		History:
14299 			Added May 15, 2021
14300 	+/
14301 	bool isMouseWheel() {
14302 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
14303 	}
14304 
14305 	// private
14306 	override void adjustClientCoordinates(int deltaX, int deltaY) {
14307 		clientX += deltaX;
14308 		clientY += deltaY;
14309 	}
14310 
14311 	override void adjustScrolling() {
14312 	version(custom_widgets) { // TEMP
14313 		viewportX = clientX;
14314 		viewportY = clientY;
14315 		if(auto se = cast(ScrollableWidget) srcElement) {
14316 			clientX += se.scrollOrigin.x;
14317 			clientY += se.scrollOrigin.y;
14318 		} else if(auto se = cast(ScrollableContainerWidget) srcElement) {
14319 			//clientX += se.scrollX_;
14320 			//clientY += se.scrollY_;
14321 		}
14322 	}
14323 	}
14324 
14325 	mixin Register;
14326 }
14327 
14328 /++
14329 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
14330 
14331 
14332 	$(WARNING
14333 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
14334 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
14335 		behavior.
14336 	)
14337 
14338 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
14339 
14340 	[MouseUpEvent] is sent when the user releases a mouse button.
14341 
14342 	[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.)
14343 
14344 	[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.
14345 
14346 	[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.
14347 
14348 	[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.
14349 
14350 	[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.
14351 
14352 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
14353 
14354 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
14355 
14356 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14357 
14358 	Rationale:
14359 
14360 		If you only want to do drag, mousedown/up works just fine being consistently sent.
14361 
14362 		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).
14363 
14364 		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.
14365 
14366 	History:
14367 		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.
14368 +/
14369 class MouseUpEvent : MouseEventBase {
14370 	enum EventString = "mouseup"; ///
14371 	this(Widget target) { super(EventString, target); }
14372 }
14373 /// ditto
14374 class MouseDownEvent : MouseEventBase {
14375 	enum EventString = "mousedown"; ///
14376 	this(Widget target) { super(EventString, target); }
14377 }
14378 /// ditto
14379 class MouseMoveEvent : MouseEventBase {
14380 	enum EventString = "mousemove"; ///
14381 	this(Widget target) { super(EventString, target); }
14382 }
14383 /// ditto
14384 class ClickEvent : MouseEventBase {
14385 	enum EventString = "click"; ///
14386 	this(Widget target) { super(EventString, target); }
14387 }
14388 /// ditto
14389 class DoubleClickEvent : MouseEventBase {
14390 	enum EventString = "dblclick"; ///
14391 	this(Widget target) { super(EventString, target); }
14392 }
14393 /// ditto
14394 class MouseOverEvent : Event {
14395 	enum EventString = "mouseover"; ///
14396 	this(Widget target) { super(EventString, target); }
14397 }
14398 /// ditto
14399 class MouseOutEvent : Event {
14400 	enum EventString = "mouseout"; ///
14401 	this(Widget target) { super(EventString, target); }
14402 }
14403 /// ditto
14404 class MouseEnterEvent : Event {
14405 	enum EventString = "mouseenter"; ///
14406 	this(Widget target) { super(EventString, target); }
14407 
14408 	override bool propagates() const { return false; }
14409 }
14410 /// ditto
14411 class MouseLeaveEvent : Event {
14412 	enum EventString = "mouseleave"; ///
14413 	this(Widget target) { super(EventString, target); }
14414 
14415 	override bool propagates() const { return false; }
14416 }
14417 
14418 private bool isAParentOf(Widget a, Widget b) {
14419 	if(a is null || b is null)
14420 		return false;
14421 
14422 	while(b !is null) {
14423 		if(a is b)
14424 			return true;
14425 		b = b.parent;
14426 	}
14427 
14428 	return false;
14429 }
14430 
14431 private struct WidgetAtPointResponse {
14432 	Widget widget;
14433 
14434 	// x, y relative to the widget in the response.
14435 	int x;
14436 	int y;
14437 }
14438 
14439 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
14440 	assert(starting !is null);
14441 
14442 	starting.addScrollPosition(x, y);
14443 
14444 	auto child = starting.getChildAtPosition(x, y);
14445 	while(child) {
14446 		if(child.hidden)
14447 			continue;
14448 		starting = child;
14449 		x -= child.x;
14450 		y -= child.y;
14451 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
14452 		child = r.widget;
14453 		if(child is starting)
14454 			break;
14455 	}
14456 	return WidgetAtPointResponse(starting, x, y);
14457 }
14458 
14459 version(win32_widgets) {
14460 private:
14461 	import core.sys.windows.commctrl;
14462 
14463 	pragma(lib, "comctl32");
14464 	shared static this() {
14465 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
14466 		INITCOMMONCONTROLSEX ic;
14467 		ic.dwSize = cast(DWORD) ic.sizeof;
14468 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
14469 		if(!InitCommonControlsEx(&ic)) {
14470 			//writeln("ICC failed");
14471 		}
14472 	}
14473 
14474 
14475 	// everything from here is just win32 headers copy pasta
14476 private:
14477 extern(Windows):
14478 
14479 	alias HANDLE HMENU;
14480 	HMENU CreateMenu();
14481 	bool SetMenu(HWND, HMENU);
14482 	HMENU CreatePopupMenu();
14483 	enum MF_POPUP = 0x10;
14484 	enum MF_STRING = 0;
14485 
14486 
14487 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
14488 	struct INITCOMMONCONTROLSEX {
14489 		DWORD dwSize;
14490 		DWORD dwICC;
14491 	}
14492 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
14493 enum {
14494         IDB_STD_SMALL_COLOR,
14495         IDB_STD_LARGE_COLOR,
14496         IDB_VIEW_SMALL_COLOR = 4,
14497         IDB_VIEW_LARGE_COLOR = 5
14498 }
14499 enum {
14500         STD_CUT,
14501         STD_COPY,
14502         STD_PASTE,
14503         STD_UNDO,
14504         STD_REDOW,
14505         STD_DELETE,
14506         STD_FILENEW,
14507         STD_FILEOPEN,
14508         STD_FILESAVE,
14509         STD_PRINTPRE,
14510         STD_PROPERTIES,
14511         STD_HELP,
14512         STD_FIND,
14513         STD_REPLACE,
14514         STD_PRINT // = 14
14515 }
14516 
14517 alias HANDLE HIMAGELIST;
14518 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
14519 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
14520         BOOL ImageList_Destroy(HIMAGELIST);
14521 
14522 uint MAKELONG(ushort a, ushort b) {
14523         return cast(uint) ((b << 16) | a);
14524 }
14525 
14526 
14527 struct TBBUTTON {
14528 	int   iBitmap;
14529 	int   idCommand;
14530 	BYTE  fsState;
14531 	BYTE  fsStyle;
14532 	version(Win64)
14533 	BYTE[6] bReserved;
14534 	else
14535 	BYTE[2]  bReserved;
14536 	DWORD dwData;
14537 	INT_PTR   iString;
14538 }
14539 
14540 	enum {
14541 		TB_ADDBUTTONSA   = WM_USER + 20,
14542 		TB_INSERTBUTTONA = WM_USER + 21,
14543 		TB_GETIDEALSIZE = WM_USER + 99,
14544 	}
14545 
14546 struct SIZE {
14547 	LONG cx;
14548 	LONG cy;
14549 }
14550 
14551 
14552 enum {
14553 	TBSTATE_CHECKED       = 1,
14554 	TBSTATE_PRESSED       = 2,
14555 	TBSTATE_ENABLED       = 4,
14556 	TBSTATE_HIDDEN        = 8,
14557 	TBSTATE_INDETERMINATE = 16,
14558 	TBSTATE_WRAP          = 32
14559 }
14560 
14561 
14562 
14563 enum {
14564 	ILC_COLOR    = 0,
14565 	ILC_COLOR4   = 4,
14566 	ILC_COLOR8   = 8,
14567 	ILC_COLOR16  = 16,
14568 	ILC_COLOR24  = 24,
14569 	ILC_COLOR32  = 32,
14570 	ILC_COLORDDB = 254,
14571 	ILC_MASK     = 1,
14572 	ILC_PALETTE  = 2048
14573 }
14574 
14575 
14576 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
14577 
14578 
14579 enum {
14580 	TB_ENABLEBUTTON          = WM_USER + 1,
14581 	TB_CHECKBUTTON,
14582 	TB_PRESSBUTTON,
14583 	TB_HIDEBUTTON,
14584 	TB_INDETERMINATE, //     = WM_USER + 5,
14585 	TB_ISBUTTONENABLED       = WM_USER + 9,
14586 	TB_ISBUTTONCHECKED,
14587 	TB_ISBUTTONPRESSED,
14588 	TB_ISBUTTONHIDDEN,
14589 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
14590 	TB_SETSTATE              = WM_USER + 17,
14591 	TB_GETSTATE              = WM_USER + 18,
14592 	TB_ADDBITMAP             = WM_USER + 19,
14593 	TB_DELETEBUTTON          = WM_USER + 22,
14594 	TB_GETBUTTON,
14595 	TB_BUTTONCOUNT,
14596 	TB_COMMANDTOINDEX,
14597 	TB_SAVERESTOREA,
14598 	TB_CUSTOMIZE,
14599 	TB_ADDSTRINGA,
14600 	TB_GETITEMRECT,
14601 	TB_BUTTONSTRUCTSIZE,
14602 	TB_SETBUTTONSIZE,
14603 	TB_SETBITMAPSIZE,
14604 	TB_AUTOSIZE, //          = WM_USER + 33,
14605 	TB_GETTOOLTIPS           = WM_USER + 35,
14606 	TB_SETTOOLTIPS           = WM_USER + 36,
14607 	TB_SETPARENT             = WM_USER + 37,
14608 	TB_SETROWS               = WM_USER + 39,
14609 	TB_GETROWS,
14610 	TB_GETBITMAPFLAGS,
14611 	TB_SETCMDID,
14612 	TB_CHANGEBITMAP,
14613 	TB_GETBITMAP,
14614 	TB_GETBUTTONTEXTA,
14615 	TB_REPLACEBITMAP, //     = WM_USER + 46,
14616 	TB_GETBUTTONSIZE         = WM_USER + 58,
14617 	TB_SETBUTTONWIDTH        = WM_USER + 59,
14618 	TB_GETBUTTONTEXTW        = WM_USER + 75,
14619 	TB_SAVERESTOREW          = WM_USER + 76,
14620 	TB_ADDSTRINGW            = WM_USER + 77,
14621 }
14622 
14623 extern(Windows)
14624 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
14625 
14626 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
14627 
14628 
14629 	enum {
14630 		TB_SETINDENT = WM_USER + 47,
14631 		TB_SETIMAGELIST,
14632 		TB_GETIMAGELIST,
14633 		TB_LOADIMAGES,
14634 		TB_GETRECT,
14635 		TB_SETHOTIMAGELIST,
14636 		TB_GETHOTIMAGELIST,
14637 		TB_SETDISABLEDIMAGELIST,
14638 		TB_GETDISABLEDIMAGELIST,
14639 		TB_SETSTYLE,
14640 		TB_GETSTYLE,
14641 		//TB_GETBUTTONSIZE,
14642 		//TB_SETBUTTONWIDTH,
14643 		TB_SETMAXTEXTROWS,
14644 		TB_GETTEXTROWS // = WM_USER + 61
14645 	}
14646 
14647 enum {
14648 	CCM_FIRST            = 0x2000,
14649 	CCM_LAST             = CCM_FIRST + 0x200,
14650 	CCM_SETBKCOLOR       = 8193,
14651 	CCM_SETCOLORSCHEME   = 8194,
14652 	CCM_GETCOLORSCHEME   = 8195,
14653 	CCM_GETDROPTARGET    = 8196,
14654 	CCM_SETUNICODEFORMAT = 8197,
14655 	CCM_GETUNICODEFORMAT = 8198,
14656 	CCM_SETVERSION       = 0x2007,
14657 	CCM_GETVERSION       = 0x2008,
14658 	CCM_SETNOTIFYWINDOW  = 0x2009
14659 }
14660 
14661 
14662 enum {
14663 	PBM_SETRANGE     = WM_USER + 1,
14664 	PBM_SETPOS,
14665 	PBM_DELTAPOS,
14666 	PBM_SETSTEP,
14667 	PBM_STEPIT,   // = WM_USER + 5
14668 	PBM_SETRANGE32   = 1030,
14669 	PBM_GETRANGE,
14670 	PBM_GETPOS,
14671 	PBM_SETBARCOLOR, // = 1033
14672 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
14673 }
14674 
14675 enum {
14676 	PBS_SMOOTH   = 1,
14677 	PBS_VERTICAL = 4
14678 }
14679 
14680 enum {
14681         ICC_LISTVIEW_CLASSES = 1,
14682         ICC_TREEVIEW_CLASSES = 2,
14683         ICC_BAR_CLASSES      = 4,
14684         ICC_TAB_CLASSES      = 8,
14685         ICC_UPDOWN_CLASS     = 16,
14686         ICC_PROGRESS_CLASS   = 32,
14687         ICC_HOTKEY_CLASS     = 64,
14688         ICC_ANIMATE_CLASS    = 128,
14689         ICC_WIN95_CLASSES    = 255,
14690         ICC_DATE_CLASSES     = 256,
14691         ICC_USEREX_CLASSES   = 512,
14692         ICC_COOL_CLASSES     = 1024,
14693 	ICC_STANDARD_CLASSES = 0x00004000,
14694 }
14695 
14696 	enum WM_USER = 1024;
14697 }
14698 
14699 version(win32_widgets)
14700 	pragma(lib, "comdlg32");
14701 
14702 
14703 ///
14704 enum GenericIcons : ushort {
14705 	None, ///
14706 	// these happen to match the win32 std icons numerically if you just subtract one from the value
14707 	Cut, ///
14708 	Copy, ///
14709 	Paste, ///
14710 	Undo, ///
14711 	Redo, ///
14712 	Delete, ///
14713 	New, ///
14714 	Open, ///
14715 	Save, ///
14716 	PrintPreview, ///
14717 	Properties, ///
14718 	Help, ///
14719 	Find, ///
14720 	Replace, ///
14721 	Print, ///
14722 }
14723 
14724 enum FileDialogType {
14725 	Automatic,
14726 	Open,
14727 	Save
14728 }
14729 string previousFileReferenced;
14730 
14731 /++
14732 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
14733 
14734 	Params:
14735 		storage = an alias to a `static string` variable that stores the last file referenced. It will
14736 		use this to pre-fill the dialog with a suggestion.
14737 
14738 		Please note that it MUST be `static` or you will get compile errors.
14739 
14740 		filters = the filters param to [getFileName]
14741 
14742 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
14743 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
14744 		a save dialog box. Otherwise, it will show an open dialog box.
14745 +/
14746 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
14747 	string name;
14748 	alias name this;
14749 }
14750 
14751 /++
14752 	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.
14753 
14754 	History:
14755 		onCancel was added November 6, 2021.
14756 
14757 		The dialog itself on Linux was modified on December 2, 2021 to include
14758 		a directory picker in addition to the command line completion view.
14759 
14760 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
14761 	Future_directions:
14762 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
14763 		at least on Linux, maybe on Windows too.
14764 +/
14765 void getOpenFileName(
14766 	void delegate(string) onOK,
14767 	string prefilledName = null,
14768 	string[] filters = null,
14769 	void delegate() onCancel = null,
14770 	string initialDirectory = null,
14771 )
14772 {
14773 	return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory);
14774 }
14775 
14776 /// ditto
14777 void getSaveFileName(
14778 	void delegate(string) onOK,
14779 	string prefilledName = null,
14780 	string[] filters = null,
14781 	void delegate() onCancel = null,
14782 	string initialDirectory = null,
14783 )
14784 {
14785 	return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory);
14786 }
14787 
14788 void getFileName(
14789 	bool openOrSave,
14790 	void delegate(string) onOK,
14791 	string prefilledName = null,
14792 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
14793 	void delegate() onCancel = null,
14794 	string initialDirectory = null,
14795 )
14796 {
14797 
14798 	version(win32_widgets) {
14799 		import core.sys.windows.commdlg;
14800 	/*
14801 	Ofn.lStructSize = sizeof(OPENFILENAME);
14802 	Ofn.hwndOwner = hWnd;
14803 	Ofn.lpstrFilter = szFilter;
14804 	Ofn.lpstrFile= szFile;
14805 	Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
14806 	Ofn.lpstrFileTitle = szFileTitle;
14807 	Ofn.nMaxFileTitle = sizeof(szFileTitle);
14808 	Ofn.lpstrInitialDir = (LPSTR)NULL;
14809 	Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
14810 	Ofn.lpstrTitle = szTitle;
14811 	 */
14812 
14813 
14814 		wchar[1024] file = 0;
14815 		wchar[1024] filterBuffer = 0;
14816 		makeWindowsString(prefilledName, file[]);
14817 		OPENFILENAME ofn;
14818 		ofn.lStructSize = ofn.sizeof;
14819 		if(filters.length) {
14820 			string filter;
14821 			foreach(i, f; filters) {
14822 				filter ~= f;
14823 				filter ~= "\0";
14824 			}
14825 			filter ~= "\0";
14826 			ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
14827 		}
14828 		ofn.lpstrFile = file.ptr;
14829 		ofn.nMaxFile = file.length;
14830 
14831 		wchar[1024] initialDir = 0;
14832 		if(initialDirectory !is null) {
14833 			makeWindowsString(initialDirectory, initialDir[]);
14834 			ofn.lpstrInitialDir = file.ptr;
14835 		}
14836 
14837 		if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
14838 		{
14839 			string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
14840 			if(okString.length && okString[$-1] == '\0')
14841 				okString = okString[0..$-1];
14842 			onOK(okString);
14843 		} else {
14844 			if(onCancel)
14845 				onCancel();
14846 		}
14847 	} else version(custom_widgets) {
14848 		if(filters.length == 0)
14849 			filters = ["All Files\0*.*"];
14850 		auto picker = new FilePicker(prefilledName, filters, initialDirectory);
14851 		picker.onOK = onOK;
14852 		picker.onCancel = onCancel;
14853 		picker.show();
14854 	}
14855 }
14856 
14857 version(custom_widgets)
14858 private
14859 class FilePicker : Dialog {
14860 	void delegate(string) onOK;
14861 	void delegate() onCancel;
14862 	LineEdit lineEdit;
14863 
14864 	// returns common prefix
14865 	string loadFiles(string cwd, string[] filters...) {
14866 		string[] files;
14867 		string[] dirs;
14868 
14869 		string commonPrefix;
14870 
14871 		getFiles(cwd, (string name, bool isDirectory) {
14872 			if(name == ".")
14873 				return; // skip this as unnecessary
14874 			if(isDirectory)
14875 				dirs ~= name;
14876 			else {
14877 				foreach(filter; filters)
14878 				if(
14879 					filter.length <= 1 ||
14880 					filter == "*.*" ||
14881 					(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
14882 					(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
14883 				)
14884 				{
14885 					files ~= name;
14886 
14887 					if(filter.length > 0 && filter[$-1] == '*') {
14888 						if(commonPrefix is null) {
14889 							commonPrefix = name;
14890 						} else {
14891 							foreach(idx, char i; name) {
14892 								if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
14893 									commonPrefix = commonPrefix[0 .. idx];
14894 									break;
14895 								}
14896 							}
14897 						}
14898 					}
14899 
14900 					break;
14901 				}
14902 			}
14903 		});
14904 
14905 		extern(C) static int comparator(scope const void* a, scope const void* b) {
14906 			auto sa = *cast(string*) a;
14907 			auto sb = *cast(string*) b;
14908 
14909 			for(int i = 0; i < sa.length; i++) {
14910 				if(i == sb.length)
14911 					return 1;
14912 				return sa[i] - sb[i];
14913 			}
14914 
14915 			return 0;
14916 		}
14917 
14918 		nonPhobosSort(files, &comparator);
14919 		nonPhobosSort(dirs, &comparator);
14920 
14921 		listWidget.clear();
14922 		dirWidget.clear();
14923 		foreach(name; dirs)
14924 			dirWidget.addOption(name);
14925 		foreach(name; files)
14926 			listWidget.addOption(name);
14927 
14928 		return commonPrefix;
14929 	}
14930 
14931 	ListWidget listWidget;
14932 	ListWidget dirWidget;
14933 
14934 	string currentDirectory;
14935 	string[] processedFilters;
14936 
14937 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"]
14938 	this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) {
14939 		super(300, 200, "Choose File..."); // owner);
14940 
14941 		foreach(filter; filters) {
14942 			while(filter.length && filter[0] != 0) {
14943 				filter = filter[1 .. $];
14944 			}
14945 			if(filter.length)
14946 				filter = filter[1 .. $]; // trim off the 0
14947 
14948 			while(filter.length) {
14949 				int idx = 0;
14950 				while(idx < filter.length && filter[idx] != ';') {
14951 					idx++;
14952 				}
14953 
14954 				processedFilters ~= filter[0 .. idx];
14955 				if(idx < filter.length)
14956 					idx++; // skip the ;
14957 				filter = filter[idx .. $];
14958 			}
14959 		}
14960 
14961 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
14962 
14963 		{
14964 			auto hl = new HorizontalLayout(this);
14965 			dirWidget = new ListWidget(hl);
14966 			listWidget = new ListWidget(hl);
14967 
14968 			// double click events normally trigger something else but
14969 			// here user might be clicking kinda fast and we'd rather just
14970 			// keep it
14971 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
14972 				auto ce = new ChangeEvent!void(dirWidget, () {});
14973 				ce.dispatch();
14974 			});
14975 
14976 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
14977 				string v;
14978 				foreach(o; dirWidget.options)
14979 					if(o.selected) {
14980 						v = o.label;
14981 						break;
14982 					}
14983 				if(v.length) {
14984 					currentDirectory ~= "/" ~ v;
14985 					loadFiles(currentDirectory, processedFilters);
14986 				}
14987 			});
14988 
14989 			// double click here, on the other hand, selects the file
14990 			// and moves on
14991 			listWidget.addEventListener((scope DoubleClickEvent dev) {
14992 				OK();
14993 			});
14994 		}
14995 
14996 		lineEdit = new LineEdit(this);
14997 		lineEdit.focus();
14998 		lineEdit.addEventListener(delegate(CharEvent event) {
14999 			if(event.character == '\t' || event.character == '\n')
15000 				event.preventDefault();
15001 		});
15002 
15003 		listWidget.addEventListener(EventType.change, () {
15004 			foreach(o; listWidget.options)
15005 				if(o.selected)
15006 					lineEdit.content = o.label;
15007 		});
15008 
15009 		loadFiles(currentDirectory, processedFilters);
15010 
15011 		lineEdit.addEventListener((KeyDownEvent event) {
15012 			if(event.key == Key.Tab) {
15013 
15014 				auto current = lineEdit.content;
15015 				if(current.length >= 2 && current[0 ..2] == "./")
15016 					current = current[2 .. $];
15017 
15018 				auto commonPrefix = loadFiles(".", current ~ "*");
15019 
15020 				if(commonPrefix.length)
15021 					lineEdit.content = commonPrefix;
15022 
15023 				// FIXME: if that is a directory, add the slash? or even go inside?
15024 
15025 				event.preventDefault();
15026 			}
15027 		});
15028 
15029 		lineEdit.content = prefilledName;
15030 
15031 		auto hl = new HorizontalLayout(60, this);
15032 		auto cancelButton = new Button("Cancel", hl);
15033 		auto okButton = new Button("OK", hl);
15034 
15035 		cancelButton.addEventListener(EventType.triggered, &Cancel);
15036 		okButton.addEventListener(EventType.triggered, &OK);
15037 
15038 		this.addEventListener((KeyDownEvent event) {
15039 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
15040 				event.preventDefault();
15041 				OK();
15042 			}
15043 			if(event.key == Key.Escape)
15044 				Cancel();
15045 		});
15046 
15047 	}
15048 
15049 	override void OK() {
15050 		if(lineEdit.content.length) {
15051 			string accepted;
15052 			auto c = lineEdit.content;
15053 			if(c.length && c[0] == '/')
15054 				accepted = c;
15055 			else
15056 				accepted = currentDirectory ~ "/" ~ lineEdit.content;
15057 
15058 			if(isDir(accepted)) {
15059 				// FIXME: would be kinda nice to support ~ and collapse these paths too
15060 				// FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later.
15061 				currentDirectory = accepted;
15062 				loadFiles(currentDirectory, processedFilters);
15063 				lineEdit.content = "";
15064 				return;
15065 			}
15066 
15067 			if(onOK)
15068 				onOK(accepted);
15069 		}
15070 		close();
15071 	}
15072 
15073 	override void Cancel() {
15074 		if(onCancel)
15075 			onCancel();
15076 		close();
15077 	}
15078 }
15079 
15080 private bool isDir(string name) {
15081 	version(Windows) {
15082 		auto ws = WCharzBuffer(name);
15083 		auto ret = GetFileAttributesW(ws.ptr);
15084 		if(ret == INVALID_FILE_ATTRIBUTES)
15085 			return false;
15086 		return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0;
15087 	} else version(Posix) {
15088 		import core.sys.posix.sys.stat;
15089 		stat_t buf;
15090 		auto ret = stat((name ~ '\0').ptr, &buf);
15091 		if(ret == -1)
15092 			return false; // I could probably check more specific errors tbh
15093 		return (buf.st_mode & S_IFMT) == S_IFDIR;
15094 	} else return false;
15095 }
15096 
15097 /*
15098 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
15099 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
15100 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
15101 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
15102 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
15103 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
15104 http://www.sbin.org/doc/Xlib/chapt_03.html
15105 
15106 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
15107 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
15108 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
15109 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
15110 */
15111 
15112 
15113 // These are all for setMenuAndToolbarFromAnnotatedCode
15114 /// This item in the menu will be preceded by a separator line
15115 /// Group: generating_from_code
15116 struct separator {}
15117 deprecated("It was misspelled, use separator instead") alias seperator = separator;
15118 /// Program-wide keyboard shortcut to trigger the action
15119 /// Group: generating_from_code
15120 struct accelerator { string keyString; }
15121 /// tells which menu the action will be on
15122 /// Group: generating_from_code
15123 struct menu { string name; }
15124 /// Describes which toolbar section the action appears on
15125 /// Group: generating_from_code
15126 struct toolbar { string groupName; }
15127 ///
15128 /// Group: generating_from_code
15129 struct icon { ushort id; }
15130 ///
15131 /// Group: generating_from_code
15132 struct label { string label; }
15133 ///
15134 /// Group: generating_from_code
15135 struct hotkey { dchar ch; }
15136 ///
15137 /// Group: generating_from_code
15138 struct tip { string tip; }
15139 
15140 
15141 /++
15142 	Observes and allows inspection of an object via automatic gui
15143 +/
15144 /// Group: generating_from_code
15145 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
15146 	return new ObjectInspectionWindowImpl!(T)(t);
15147 }
15148 
15149 class ObjectInspectionWindow : Window {
15150 	this(int a, int b, string c) {
15151 		super(a, b, c);
15152 	}
15153 
15154 	abstract void readUpdatesFromObject();
15155 }
15156 
15157 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
15158 	T t;
15159 	this(T t) {
15160 		this.t = t;
15161 
15162 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
15163 
15164 		foreach(memberName; __traits(derivedMembers, T)) {{
15165 			alias member = I!(__traits(getMember, t, memberName))[0];
15166 			alias type = typeof(member);
15167 			static if(is(type == int)) {
15168 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
15169 				//le.addEventListener("char", (Event ev) {
15170 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
15171 						//ev.preventDefault();
15172 				//});
15173 				le.addEventListener(EventType.change, (Event ev) {
15174 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
15175 				});
15176 
15177 				updateMemberDelegates[memberName] = () {
15178 					le.content = toInternal!string(__traits(getMember, t, memberName));
15179 				};
15180 			}
15181 		}}
15182 	}
15183 
15184 	void delegate()[string] updateMemberDelegates;
15185 
15186 	override void readUpdatesFromObject() {
15187 		foreach(k, v; updateMemberDelegates)
15188 			v();
15189 	}
15190 }
15191 
15192 /++
15193 	Creates a dialog based on a data structure.
15194 
15195 	---
15196 	dialog((YourStructure value) {
15197 		// the user filled in the struct and clicked OK,
15198 		// you can check the members now
15199 	});
15200 	---
15201 
15202 	Params:
15203 		initialData = the initial value to show in the dialog. It will not modify this unless
15204 		it is a class then it might, no promises.
15205 
15206 	History:
15207 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
15208 +/
15209 /// Group: generating_from_code
15210 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15211 	dialog(T.init, onOK, onCancel, title);
15212 }
15213 /// ditto
15214 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15215 	auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title);
15216 	dg.show();
15217 }
15218 
15219 private static template I(T...) { alias I = T; }
15220 
15221 
15222 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
15223 	if(name == "id")
15224 		return allLowerCase ? name : "ID";
15225 
15226 	char[160] buffer;
15227 	int bufferIndex = 0;
15228 	bool shouldCap = true;
15229 	bool shouldSpace;
15230 	bool lastWasCap;
15231 	foreach(idx, char ch; name) {
15232 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15233 
15234 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
15235 			if(lastWasCap) {
15236 				// two caps in a row, don't change. Prolly acronym.
15237 			} else {
15238 				if(idx)
15239 					shouldSpace = true; // new word, add space
15240 			}
15241 
15242 			lastWasCap = true;
15243 		} else {
15244 			lastWasCap = false;
15245 		}
15246 
15247 		if(shouldSpace) {
15248 			buffer[bufferIndex++] = space;
15249 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15250 			shouldSpace = false;
15251 		}
15252 		if(shouldCap) {
15253 			if(ch >= 'a' && ch <= 'z')
15254 				ch -= 32;
15255 			shouldCap = false;
15256 		}
15257 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
15258 			ch += 32;
15259 		buffer[bufferIndex++] = ch;
15260 	}
15261 	return buffer[0 .. bufferIndex].idup;
15262 }
15263 
15264 /++
15265 	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.
15266 +/
15267 class AutomaticDialog(T) : Dialog {
15268 	T t;
15269 
15270 	void delegate(T) onOK;
15271 	void delegate() onCancel;
15272 
15273 	override int paddingTop() { return defaultLineHeight; }
15274 	override int paddingBottom() { return defaultLineHeight; }
15275 	override int paddingRight() { return defaultLineHeight; }
15276 	override int paddingLeft() { return defaultLineHeight; }
15277 
15278 	this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
15279 		assert(onOK !is null);
15280 
15281 		t = initialData;
15282 
15283 		static if(is(T == class)) {
15284 			if(t is null)
15285 				t = new T();
15286 		}
15287 		this.onOK = onOK;
15288 		this.onCancel = onCancel;
15289 		super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
15290 
15291 		static if(is(T == class))
15292 			this.addDataControllerWidget(t);
15293 		else
15294 			this.addDataControllerWidget(&t);
15295 
15296 		auto hl = new HorizontalLayout(this);
15297 		auto stretch = new HorizontalSpacer(hl); // to right align
15298 		auto ok = new CommandButton("OK", hl);
15299 		auto cancel = new CommandButton("Cancel", hl);
15300 		ok.addEventListener(EventType.triggered, &OK);
15301 		cancel.addEventListener(EventType.triggered, &Cancel);
15302 
15303 		this.addEventListener((KeyDownEvent ev) {
15304 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
15305 				ok.focus();
15306 				OK();
15307 				ev.preventDefault();
15308 			}
15309 			if(ev.key == Key.Escape) {
15310 				Cancel();
15311 				ev.preventDefault();
15312 			}
15313 		});
15314 
15315 		this.addEventListener((scope ClosedEvent ce) {
15316 			if(onCancel)
15317 				onCancel();
15318 		});
15319 
15320 		//this.children[0].focus();
15321 	}
15322 
15323 	override void OK() {
15324 		onOK(t);
15325 		close();
15326 	}
15327 
15328 	override void Cancel() {
15329 		if(onCancel)
15330 			onCancel();
15331 		close();
15332 	}
15333 }
15334 
15335 private template baseClassCount(Class) {
15336 	private int helper() {
15337 		int count = 0;
15338 		static if(is(Class bases == super)) {
15339 			foreach(base; bases)
15340 				static if(is(base == class))
15341 					count += 1 + baseClassCount!base;
15342 		}
15343 		return count;
15344 	}
15345 
15346 	enum int baseClassCount = helper();
15347 }
15348 
15349 private long stringToLong(string s) {
15350 	long ret;
15351 	if(s.length == 0)
15352 		return ret;
15353 	bool negative = s[0] == '-';
15354 	if(negative)
15355 		s = s[1 .. $];
15356 	foreach(ch; s) {
15357 		if(ch >= '0' && ch <= '9') {
15358 			ret *= 10;
15359 			ret += ch - '0';
15360 		}
15361 	}
15362 	if(negative)
15363 		ret = -ret;
15364 	return ret;
15365 }
15366 
15367 
15368 interface ReflectableProperties {
15369 	/++
15370 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
15371 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
15372 		json in the current implementation.
15373 
15374 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
15375 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
15376 		as of the June 2, 2021 release.
15377 
15378 		History:
15379 			Added June 2, 2021.
15380 
15381 		See_Also: [getPropertyAsString], [setPropertyFromString]
15382 	+/
15383 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
15384 	/++
15385 		Requests a property to be delivered to you as a string, through your `sink` delegate.
15386 
15387 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
15388 		be interpreted as json, otherwise, it is just a plain string.
15389 
15390 		The sink should always be called exactly once for each call (it is basically a return value, but it might
15391 		use a local buffer it maintains instead of allocating a return value).
15392 
15393 		History:
15394 			Added June 2, 2021.
15395 
15396 		See_Also: [getPropertiesList], [setPropertyFromString]
15397 	+/
15398 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
15399 	/++
15400 		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.
15401 
15402 		History:
15403 			Added June 2, 2021.
15404 
15405 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
15406 	+/
15407 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
15408 
15409 	/// [setPropertyFromString] possible return values
15410 	enum SetPropertyResult {
15411 		success = 0, /// the property has been successfully set to the request value
15412 		notPermitted = -1, /// the property exists but it cannot be changed at this time
15413 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
15414 		noSuchProperty = -3, /// there is no property by that name
15415 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
15416 		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)
15417 	}
15418 
15419 	/++
15420 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
15421 
15422 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
15423 
15424 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
15425 		rarely need to use these building blocks directly.
15426 	+/
15427 	mixin template RegisterSetters() {
15428 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
15429 			switch(name) {
15430 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
15431 					case memberName:
15432 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
15433 							if(value != "true" && value != "false")
15434 								return SetPropertyResult.wrongFormat;
15435 							__traits(getMember, this, memberName) = value == "true" ? true : false;
15436 							return SetPropertyResult.success;
15437 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
15438 							import core.stdc.stdlib;
15439 							char[128] zero = 0;
15440 							if(buffer.length + 1 >= zero.length)
15441 								return SetPropertyResult.wrongFormat;
15442 							zero[0 .. buffer.length] = buffer[];
15443 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
15444 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
15445 							import core.stdc.stdlib;
15446 							char[128] zero = 0;
15447 							if(buffer.length + 1 >= zero.length)
15448 								return SetPropertyResult.wrongFormat;
15449 							zero[0 .. buffer.length] = buffer[];
15450 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
15451 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
15452 							__traits(getMember, this, memberName) = value.idup;
15453 						} else {
15454 							return SetPropertyResult.notImplemented;
15455 						}
15456 
15457 				}
15458 				default:
15459 					return super.setPropertyFromString(name, value, valueIsJson);
15460 			}
15461 		}
15462 	}
15463 
15464 	/++
15465 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
15466 
15467 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
15468 
15469 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
15470 		rarely need to use these building blocks directly.
15471 	+/
15472 	mixin template RegisterGetters() {
15473 		override void getPropertiesList(scope void delegate(string name) sink) const {
15474 			super.getPropertiesList(sink);
15475 
15476 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
15477 				sink(memberName);
15478 			}
15479 		}
15480 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
15481 			switch(name) {
15482 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
15483 					case memberName:
15484 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
15485 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
15486 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
15487 							import core.stdc.stdio;
15488 							char[32] buffer;
15489 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
15490 							sink(name, buffer[0 .. len], true);
15491 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
15492 							import core.stdc.stdio;
15493 							char[32] buffer;
15494 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
15495 							sink(name, buffer[0 .. len], true);
15496 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
15497 							sink(name, __traits(getMember, this, memberName), false);
15498 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
15499 						} else {
15500 							sink(name, null, true);
15501 						}
15502 
15503 					return;
15504 				}
15505 				default:
15506 					return super.getPropertyAsString(name, sink);
15507 			}
15508 		}
15509 	}
15510 }
15511 
15512 private struct Stack(T) {
15513 	this(int maxSize) {
15514 		internalLength = 0;
15515 		arr = initialBuffer[];
15516 	}
15517 
15518 	///.
15519 	void push(T t) {
15520 		if(internalLength >= arr.length) {
15521 			auto oldarr = arr;
15522 			if(arr.length < 4096)
15523 				arr = new T[arr.length * 2];
15524 			else
15525 				arr = new T[arr.length + 4096];
15526 			arr[0 .. oldarr.length] = oldarr[];
15527 		}
15528 
15529 		arr[internalLength] = t;
15530 		internalLength++;
15531 	}
15532 
15533 	///.
15534 	T pop() {
15535 		assert(internalLength);
15536 		internalLength--;
15537 		return arr[internalLength];
15538 	}
15539 
15540 	///.
15541 	T peek() {
15542 		assert(internalLength);
15543 		return arr[internalLength - 1];
15544 	}
15545 
15546 	///.
15547 	@property bool empty() {
15548 		return internalLength ? false : true;
15549 	}
15550 
15551 	///.
15552 	private T[] arr;
15553 	private size_t internalLength;
15554 	private T[64] initialBuffer;
15555 	// 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),
15556 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
15557 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
15558 }
15559 
15560 /// 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.
15561 private struct WidgetStream {
15562 
15563 	///.
15564 	@property Widget front() {
15565 		return current.widget;
15566 	}
15567 
15568 	/// Use Widget.tree instead.
15569 	this(Widget start) {
15570 		current.widget = start;
15571 		current.childPosition = -1;
15572 		isEmpty = false;
15573 		stack = typeof(stack)(0);
15574 	}
15575 
15576 	/*
15577 		Handle it
15578 		handle its children
15579 
15580 	*/
15581 
15582 	///.
15583 	void popFront() {
15584 	    more:
15585 	    	if(isEmpty) return;
15586 
15587 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
15588 
15589 		current.childPosition++;
15590 		if(current.childPosition >= current.widget.children.length) {
15591 			if(stack.empty())
15592 				isEmpty = true;
15593 			else {
15594 				current = stack.pop();
15595 				goto more;
15596 			}
15597 		} else {
15598 			stack.push(current);
15599 			current.widget = current.widget.children[current.childPosition];
15600 			current.childPosition = -1;
15601 		}
15602 	}
15603 
15604 	///.
15605 	@property bool empty() {
15606 		return isEmpty;
15607 	}
15608 
15609 	private:
15610 
15611 	struct Current {
15612 		Widget widget;
15613 		int childPosition;
15614 	}
15615 
15616 	Current current;
15617 
15618 	Stack!(Current) stack;
15619 
15620 	bool isEmpty;
15621 }
15622 
15623 
15624 /+
15625 
15626 	I could fix up the hierarchy kinda like this
15627 
15628 	class Widget {
15629 		Widget[] children() { return null; }
15630 	}
15631 	interface WidgetContainer {
15632 		Widget asWidget();
15633 		void addChild(Widget w);
15634 
15635 		// alias asWidget this; // but meh
15636 	}
15637 
15638 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
15639 
15640 	class Layout : Widget, WidgetContainer {}
15641 
15642 	class Window : WidgetContainer {}
15643 
15644 
15645 	All constructors that previously took Widgets should now take WidgetContainers instead
15646 
15647 
15648 
15649 	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".
15650 +/
15651 
15652 /+
15653 	LAYOUTS 2.0
15654 
15655 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
15656 
15657 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
15658 
15659 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
15660 
15661 	and even Paint can just use computedStyle...
15662 
15663 		background color
15664 		font
15665 		border color and style
15666 
15667 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
15668 		please note that many widgets and in some modes will completely ignore properties as they will.
15669 		they are just hints you set, not promises.
15670 
15671 
15672 
15673 
15674 
15675 	So generally the existing virtual functions are just the default for the class. But individual objects
15676 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
15677 +/
15678 
15679 /++
15680 	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.
15681 
15682 	History:
15683 		Added May 24, 2021.
15684 +/
15685 struct WidgetBackground {
15686 	/++
15687 		A background with the given solid color.
15688 	+/
15689 	this(Color color) {
15690 		this.color = color;
15691 	}
15692 
15693 	this(WidgetBackground bg) {
15694 		this = bg;
15695 	}
15696 
15697 	/++
15698 		Creates a widget from the string.
15699 
15700 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
15701 	+/
15702 	static WidgetBackground fromString(string s) {
15703 		return WidgetBackground(Color.fromString(s));
15704 	}
15705 
15706 	/++
15707 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
15708 
15709 		History:
15710 			Made `public` on December 18, 2022 (dub v10.10).
15711 	+/
15712 	Color color;
15713 }
15714 
15715 /++
15716 	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!)
15717 
15718 	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.
15719 
15720 	You should not inherit from this directly, but instead use [VisualTheme].
15721 
15722 	History:
15723 		Added May 8, 2021
15724 +/
15725 abstract class BaseVisualTheme {
15726 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
15727 	abstract void doPaint(Widget widget, WidgetPainter painter);
15728 
15729 	/+
15730 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
15731 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
15732 	+/
15733 
15734 	/++
15735 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
15736 		where the interpretation of the string varies for each property and may include things like measurement units.
15737 	+/
15738 	abstract string getPropertyString(Widget widget, string propertyName);
15739 
15740 	/++
15741 		Default background color of the window. Widgets also use this to simulate transparency.
15742 
15743 		Probably some shade of grey.
15744 	+/
15745 	abstract Color windowBackgroundColor();
15746 	abstract Color widgetBackgroundColor();
15747 	abstract Color foregroundColor();
15748 	abstract Color lightAccentColor();
15749 	abstract Color darkAccentColor();
15750 
15751 	/++
15752 		Colors used to indicate active selections in lists and text boxes, etc.
15753 	+/
15754 	abstract Color selectionForegroundColor();
15755 	/// ditto
15756 	abstract Color selectionBackgroundColor();
15757 
15758 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
15759 
15760 	/++
15761 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
15762 	+/
15763 	abstract OperatingSystemFont defaultFont(int dpi);
15764 
15765 	private OperatingSystemFont[int] defaultFontCache_;
15766 	private OperatingSystemFont defaultFontCached(int dpi) {
15767 		if(dpi !in defaultFontCache_) {
15768 			// FIXME: set this to false if X disconnect or if visual theme changes
15769 			defaultFontCache_[dpi] = defaultFont(dpi);
15770 		}
15771 		return defaultFontCache_[dpi];
15772 	}
15773 }
15774 
15775 /+
15776 	A widget should have:
15777 		classList
15778 		dataset
15779 		attributes
15780 		computedStyles
15781 		state (persistent)
15782 		dynamic state (focused, hover, etc)
15783 +/
15784 
15785 // visualTheme.computedStyle(this).paddingLeft
15786 
15787 
15788 /++
15789 	This is your entry point to create your own visual theme for custom widgets.
15790 
15791 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
15792 
15793 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
15794 +/
15795 abstract class VisualTheme(CRTP) : BaseVisualTheme {
15796 	override string getPropertyString(Widget widget, string propertyName) {
15797 		return null;
15798 	}
15799 
15800 	/+
15801 		mixin StyleOverride!Widget
15802 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
15803 		w.useStyleProperties(dg);
15804 	}
15805 	+/
15806 
15807 	final override void doPaint(Widget widget, WidgetPainter painter) {
15808 		auto derived = cast(CRTP) cast(void*) this;
15809 
15810 		scope void delegate(Widget, WidgetPainter) bestMatch;
15811 		int bestMatchScore;
15812 
15813 		static if(__traits(hasMember, CRTP, "paint"))
15814 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
15815 			static if(is(typeof(overload) Params == __parameters)) {
15816 				static assert(Params.length == 2);
15817 				static assert(is(Params[0] : Widget));
15818 				static assert(is(Params[1] == WidgetPainter));
15819 				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);
15820 
15821 				alias type = Params[0];
15822 				if(cast(type) widget) {
15823 					auto score = baseClassCount!type;
15824 
15825 					if(score > bestMatchScore) {
15826 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
15827 						bestMatchScore = score;
15828 					}
15829 				}
15830 			} else static assert(0, "paint should be a method.");
15831 		}
15832 
15833 		if(bestMatch)
15834 			bestMatch(widget, painter);
15835 		else
15836 			widget.paint(painter);
15837 	}
15838 
15839 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
15840 
15841 	// 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
15842 	// mixin Beautiful95Theme;
15843 	mixin DefaultLightTheme;
15844 
15845 	private static struct Cached {
15846 		// i prolly want to do this
15847 	}
15848 }
15849 
15850 /// ditto
15851 mixin template Beautiful95Theme() {
15852 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
15853 	override Color widgetBackgroundColor() { return Color.white; }
15854 	override Color foregroundColor() { return Color.black; }
15855 	override Color darkAccentColor() { return Color(172, 172, 172); }
15856 	override Color lightAccentColor() { return Color(223, 223, 223); }
15857 	override Color selectionForegroundColor() { return Color.white; }
15858 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
15859 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
15860 }
15861 
15862 /// ditto
15863 mixin template DefaultLightTheme() {
15864 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
15865 	override Color widgetBackgroundColor() { return Color.white; }
15866 	override Color foregroundColor() { return Color.black; }
15867 	override Color darkAccentColor() { return Color(172, 172, 172); }
15868 	override Color lightAccentColor() { return Color(223, 223, 223); }
15869 	override Color selectionForegroundColor() { return Color.white; }
15870 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
15871 	override OperatingSystemFont defaultFont(int dpi) {
15872 		version(Windows)
15873 			return new OperatingSystemFont("Segoe UI");
15874 		else {
15875 			// FIXME: undo xft's scaling so we don't end up double scaled
15876 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
15877 		}
15878 	}
15879 }
15880 
15881 /// ditto
15882 mixin template DefaultDarkTheme() {
15883 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
15884 	override Color widgetBackgroundColor() { return Color.black; }
15885 	override Color foregroundColor() { return Color.white; }
15886 	override Color darkAccentColor() { return Color(20, 20, 20); }
15887 	override Color lightAccentColor() { return Color(80, 80, 80); }
15888 	override Color selectionForegroundColor() { return Color.white; }
15889 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
15890 	override OperatingSystemFont defaultFont(int dpi) {
15891 		version(Windows)
15892 			return new OperatingSystemFont("Segoe UI", 12);
15893 		else
15894 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
15895 	}
15896 }
15897 
15898 /// ditto
15899 alias DefaultTheme = DefaultLightTheme;
15900 
15901 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
15902 	/+
15903 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
15904 	Color windowBackgroundColor() { return Color(242, 242, 242); }
15905 	Color darkAccentColor() { return windowBackgroundColor; }
15906 	Color lightAccentColor() { return windowBackgroundColor; }
15907 	+/
15908 }
15909 
15910 /++
15911 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
15912 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
15913 
15914 	History:
15915 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
15916 +/
15917 class StateChanged(alias field) : Event {
15918 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
15919 	override bool cancelable() const { return false; }
15920 	this(Widget target, typeof(field) newValue) {
15921 		this.newValue = newValue;
15922 		super(EventString, target);
15923 	}
15924 
15925 	typeof(field) newValue;
15926 }
15927 
15928 /++
15929 	Convenience function to add a `triggered` event listener.
15930 
15931 	Its implementation is simply `w.addEventListener("triggered", dg);`
15932 
15933 	History:
15934 		Added November 27, 2021 (dub v10.4)
15935 +/
15936 void addWhenTriggered(Widget w, void delegate() dg) {
15937 	w.addEventListener("triggered", dg);
15938 }
15939 
15940 /++
15941 	Observable varables can be added to widgets and when they are changed, it fires
15942 	off a [StateChanged] event so you can react to it.
15943 
15944 	It is implemented as a getter and setter property, along with another helper you
15945 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
15946 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
15947 	example.
15948 
15949 	History:
15950 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
15951 +/
15952 mixin template Observable(T, string name) {
15953 	private T backing;
15954 
15955 	mixin(q{
15956 		void } ~ name ~ q{_changed (void delegate(T) dg) {
15957 			this.addEventListener((StateChanged!this_thing ev) {
15958 				dg(ev.newValue);
15959 			});
15960 		}
15961 
15962 		@property T } ~ name ~ q{ () {
15963 			return backing;
15964 		}
15965 
15966 		@property void } ~ name ~ q{ (T t) {
15967 			backing = t;
15968 			auto event = new StateChanged!this_thing(this, t);
15969 			event.dispatch();
15970 		}
15971 	});
15972 
15973 	mixin("private alias this_thing = " ~ name ~ ";");
15974 }
15975 
15976 
15977 private bool startsWith(string test, string thing) {
15978 	if(test.length < thing.length)
15979 		return false;
15980 	return test[0 .. thing.length] == thing;
15981 }
15982 
15983 private bool endsWith(string test, string thing) {
15984 	if(test.length < thing.length)
15985 		return false;
15986 	return test[$ - thing.length .. $] == thing;
15987 }
15988 
15989 // still do layout delegation
15990 // and... split off Window from Widget.
15991 
15992 version(minigui_screenshots)
15993 struct Screenshot {
15994 	string name;
15995 }
15996 
15997 version(minigui_screenshots)
15998 static if(__VERSION__ > 2092)
15999 mixin(q{
16000 shared static this() {
16001 	import core.runtime;
16002 
16003 	static UnitTestResult screenshotMagic() {
16004 		string name;
16005 
16006 		import arsd.png;
16007 
16008 		auto results = new Window();
16009 		auto button = new Button("do it", results);
16010 
16011 		Window.newWindowCreated = delegate(Window w) {
16012 			Timer timer;
16013 			timer = new Timer(250, {
16014 				auto img = w.win.takeScreenshot();
16015 				timer.destroy();
16016 
16017 				version(Windows)
16018 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
16019 				else
16020 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
16021 
16022 				w.close();
16023 			});
16024 		};
16025 
16026 		button.addWhenTriggered( {
16027 
16028 		foreach(test; __traits(getUnitTests, mixin(__MODULE__))) {
16029 			name = null;
16030 			static foreach(attr; __traits(getAttributes, test)) {
16031 				static if(is(typeof(attr) == Screenshot))
16032 					name = attr.name;
16033 			}
16034 			if(name.length) {
16035 				test();
16036 			}
16037 		}
16038 
16039 		});
16040 
16041 		results.loop();
16042 
16043 		return UnitTestResult(0, 0, false, false);
16044 	}
16045 
16046 
16047 	Runtime.extendedModuleUnitTester = &screenshotMagic;
16048 }
16049 });
16050 version(minigui_screenshots) {
16051 	version(unittest)
16052 		void main() {}
16053 	else static assert(0, "dont forget the -unittest flag to dmd");
16054 }
16055 
16056 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
16057 // FIXME: make multiple accelerators disambiguate based ona rgs
16058 // FIXME: MainWindow ctor should have same arg order as Window
16059 // FIXME: mainwindow ctor w/ client area size instead of total size.
16060 // 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.
16061 // FIXME: tri-state checkbox
16062 // FIXME: subordinate controls grouping...