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, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn
1603 				this.redraw();
1604 			}
1605 		}
1606 		sendResizeEvent();
1607 	}
1608 
1609 	/// Creates the widget and adds it to the parent.
1610 	this(Widget parent) {
1611 		if(parent !is null)
1612 			parent.addChild(this);
1613 		setupDefaultEventHandlers();
1614 	}
1615 
1616 	/// 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.
1617 	@scriptable
1618 	bool isFocused() {
1619 		return parentWindow && parentWindow.focusedWidget is this;
1620 	}
1621 
1622 	private bool showing_ = true;
1623 	///
1624 	bool showing() { return showing_; }
1625 	///
1626 	bool hidden() { return !showing_; }
1627 	/++
1628 		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.
1629 	+/
1630 	void showing(bool s, bool recalculate = true) {
1631 		auto so = showing_;
1632 		showing_ = s;
1633 		if(s != so) {
1634 			version(win32_widgets)
1635 			if(hwnd)
1636 				ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE);
1637 
1638 			if(parent && recalculate) {
1639 				parent.queueRecomputeChildLayout();
1640 				parent.redraw();
1641 			}
1642 
1643 			foreach(child; children)
1644 				child.showing(s, false);
1645 
1646 		}
1647 		queueRecomputeChildLayout();
1648 		redraw();
1649 	}
1650 	/// Convenience method for `showing = true`
1651 	@scriptable
1652 	void show() {
1653 		showing = true;
1654 	}
1655 	/// Convenience method for `showing = false`
1656 	@scriptable
1657 	void hide() {
1658 		showing = false;
1659 	}
1660 
1661 	///
1662 	@scriptable
1663 	void focus() {
1664 		assert(parentWindow !is null);
1665 		if(isFocused())
1666 			return;
1667 
1668 		if(parentWindow.focusedWidget) {
1669 			// FIXME: more details here? like from and to
1670 			auto from = parentWindow.focusedWidget;
1671 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
1672 			parentWindow.focusedWidget = null;
1673 			from.emit!BlurEvent();
1674 			this.emit!FocusOutEvent();
1675 		}
1676 
1677 
1678 		version(win32_widgets) {
1679 			if(this.hwnd !is null)
1680 				SetFocus(this.hwnd);
1681 		}
1682 		//else static if(UsingSimpledisplayX11)
1683 			//this.parentWindow.win.focus();
1684 
1685 		parentWindow.focusedWidget = this;
1686 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
1687 		this.emit!FocusEvent();
1688 		this.emit!FocusInEvent();
1689 	}
1690 
1691 	/+
1692 	/++
1693 		Unfocuses the widget. This may reset
1694 	+/
1695 	@scriptable
1696 	void blur() {
1697 
1698 	}
1699 	+/
1700 
1701 
1702 	/++
1703 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
1704 
1705 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
1706 	+/
1707 	void attachedToWindow(Window w) {}
1708 	/++
1709 		Callback when the widget is added to another widget.
1710 
1711 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
1712 	+/
1713 	void addedTo(Widget w) {}
1714 
1715 	/++
1716 		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.
1717 
1718 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
1719 	+/
1720 	protected void addChild(Widget w, int position = int.max) {
1721 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
1722 		assert(w !is this, "Child cannot be its own parent!");
1723 		w._parent = this;
1724 		if(position == int.max || position == children.length) {
1725 			_children ~= w;
1726 		} else {
1727 			assert(position < _children.length);
1728 			_children.length = _children.length + 1;
1729 			for(int i = cast(int) _children.length - 1; i > position; i--)
1730 				_children[i] = _children[i - 1];
1731 			_children[position] = w;
1732 		}
1733 
1734 		this.parentWindow = this._parentWindow;
1735 
1736 		w.addedTo(this);
1737 
1738 		if(this.hidden)
1739 			w.showing = false;
1740 
1741 		if(parentWindow !is null) {
1742 			w.attachedToWindow(parentWindow);
1743 			parentWindow.queueRecomputeChildLayout();
1744 			parentWindow.redraw();
1745 		}
1746 	}
1747 
1748 	/++
1749 		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.
1750 	+/
1751 	Widget getChildAtPosition(int x, int y) {
1752 		// it goes backward so the last one to show gets picked first
1753 		// might use z-index later
1754 		foreach_reverse(child; children) {
1755 			if(child.hidden)
1756 				continue;
1757 			if(child.x <= x && child.y <= y
1758 				&& ((x - child.x) < child.width)
1759 				&& ((y - child.y) < child.height))
1760 			{
1761 				return child;
1762 			}
1763 		}
1764 
1765 		return null;
1766 	}
1767 
1768 	/++
1769 		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.
1770 
1771 		History:
1772 			Added July 2, 2021 (v10.2)
1773 	+/
1774 	protected void addScrollPosition(ref int x, ref int y) {};
1775 
1776 	/++
1777 		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.
1778 
1779 		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.
1780 
1781 		[paint] is not called for system widgets as the OS library draws them instead.
1782 
1783 
1784 		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.
1785 
1786 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
1787 
1788 		History:
1789 			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.
1790 	+/
1791 	void paint(WidgetPainter painter) {
1792 		version(win32_widgets)
1793 			if(hwnd) {
1794 				return;
1795 			}
1796 		painter.drawThemed(&paintContent); // note this refers to the following overload
1797 	}
1798 
1799 	/++
1800 		Responsible for drawing the content as the theme engine is responsible for other elements.
1801 
1802 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
1803 
1804 		Params:
1805 			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.
1806 
1807 			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.
1808 
1809 			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.
1810 
1811 		Returns:
1812 			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.
1813 
1814 		History:
1815 			Added May 15, 2021
1816 	+/
1817 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
1818 		return bounds;
1819 	}
1820 
1821 	deprecated("Change ScreenPainter to WidgetPainter")
1822 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
1823 
1824 	/// I don't actually like the name of this
1825 	/// this draws a background on it
1826 	void erase(WidgetPainter painter) {
1827 		version(win32_widgets)
1828 			if(hwnd) return; // Windows will do it. I think.
1829 
1830 		auto c = getComputedStyle().background.color;
1831 		painter.fillColor = c;
1832 		painter.outlineColor = c;
1833 
1834 		version(win32_widgets) {
1835 			HANDLE b, p;
1836 			if(c.a == 0 && parent is parentWindow) {
1837 				// I don't remember why I had this really...
1838 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
1839 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
1840 			}
1841 		}
1842 		painter.drawRectangle(Point(0, 0), width, height);
1843 		version(win32_widgets) {
1844 			if(c.a == 0 && parent is parentWindow) {
1845 				SelectObject(painter.impl.hdc, p);
1846 				SelectObject(painter.impl.hdc, b);
1847 			}
1848 		}
1849 	}
1850 
1851 	///
1852 	WidgetPainter draw() {
1853 		int x = this.x, y = this.y;
1854 		auto parent = this.parent;
1855 		while(parent) {
1856 			x += parent.x;
1857 			y += parent.y;
1858 			parent = parent.parent;
1859 		}
1860 
1861 		auto painter = parentWindow.win.draw(true);
1862 		painter.originX = x;
1863 		painter.originY = y;
1864 		painter.setClipRectangle(Point(0, 0), width, height);
1865 		return WidgetPainter(painter, this);
1866 	}
1867 
1868 	/// 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.
1869 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
1870 		if(hidden)
1871 			return;
1872 
1873 		int paintX = x;
1874 		int paintY = y;
1875 		if(this.useNativeDrawing()) {
1876 			paintX = 0;
1877 			paintY = 0;
1878 			lox = 0;
1879 			loy = 0;
1880 			containment = Rectangle(0, 0, int.max, int.max);
1881 		}
1882 
1883 		painter.originX = lox + paintX;
1884 		painter.originY = loy + paintY;
1885 
1886 		bool actuallyPainted = false;
1887 
1888 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
1889 		if(clip == Rectangle.init) {
1890 			// writeln(this, " clipped out");
1891 			return;
1892 		}
1893 
1894 		bool invalidateChildren = invalidate;
1895 
1896 		if(redrawRequested || force) {
1897 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
1898 
1899 			painter.drawingUpon = this;
1900 
1901 			erase(painter);
1902 			if(painter.visualTheme)
1903 				painter.visualTheme.doPaint(this, painter);
1904 			else
1905 				paint(painter);
1906 
1907 			if(invalidate) {
1908 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
1909 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
1910 				painter.invalidateRect(region);
1911 				// children are contained inside this, so no need to do extra work
1912 				invalidateChildren = false;
1913 			}
1914 
1915 			redrawRequested = false;
1916 			actuallyPainted = true;
1917 		}
1918 
1919 		foreach(child; children) {
1920 			version(win32_widgets)
1921 				if(child.useNativeDrawing()) continue;
1922 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
1923 		}
1924 
1925 		version(win32_widgets)
1926 		foreach(child; children) {
1927 			if(child.useNativeDrawing) {
1928 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
1929 				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
1930 			}
1931 		}
1932 	}
1933 
1934 	protected bool useNativeDrawing() nothrow {
1935 		version(win32_widgets)
1936 			return hwnd !is null;
1937 		else
1938 			return false;
1939 	}
1940 
1941 	private static class RedrawEvent {}
1942 	private __gshared re = new RedrawEvent();
1943 
1944 	private bool redrawRequested;
1945 	///
1946 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
1947 		redrawRequested = true;
1948 
1949 		if(this.parentWindow) {
1950 			auto sw = this.parentWindow.win;
1951 			assert(sw !is null);
1952 			if(!sw.eventQueued!RedrawEvent) {
1953 				sw.postEvent(re);
1954 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1955 			}
1956 		}
1957 	}
1958 
1959 	private SimpleWindow drawableWindow;
1960 
1961 	/++
1962 		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.
1963 
1964 		Returns:
1965 			`true` if you should do your default behavior.
1966 
1967 		History:
1968 			Added May 5, 2021
1969 
1970 		Bugs:
1971 			It does not do the static checks on gdc right now.
1972 	+/
1973 	final protected bool emit(EventType, this This, Args...)(Args args) {
1974 		version(GNU) {} else
1975 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
1976 		auto e = new EventType(this, args);
1977 		e.dispatch();
1978 		return !e.defaultPrevented;
1979 	}
1980 	/// ditto
1981 	final protected bool emit(string eventString, this This)() {
1982 		auto e = new Event(eventString, this);
1983 		e.dispatch();
1984 		return !e.defaultPrevented;
1985 	}
1986 
1987 	/++
1988 		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.
1989 
1990 		History:
1991 			Added May 5, 2021
1992 	+/
1993 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
1994 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
1995 		return addEventListener(handler);
1996 	}
1997 
1998 	/++
1999 		Gets the computed style properties from the visual theme.
2000 
2001 		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].)
2002 
2003 		History:
2004 			Added May 8, 2021
2005 	+/
2006 	final StyleInformation getComputedStyle() {
2007 		return StyleInformation(this);
2008 	}
2009 
2010 	int focusableWidgets(scope int delegate(Widget) dg) {
2011 		foreach(widget; WidgetStream(this)) {
2012 			if(widget.tabStop && !widget.hidden) {
2013 				int result = dg(widget);
2014 				if (result)
2015 					return result;
2016 			}
2017 		}
2018 		return 0;
2019 	}
2020 
2021 	/++
2022 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2023 		for the given content box (the area between the padding)
2024 
2025 		History:
2026 			Added January 4, 2023 (dub v11.0)
2027 	+/
2028 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2029 		auto cs = getComputedStyle();
2030 
2031 		auto borderWidth = getBorderWidth(cs.borderStyle);
2032 
2033 		auto rect = contentBox;
2034 
2035 		rect.left -= borderWidth;
2036 		rect.right += borderWidth;
2037 		rect.top -= borderWidth;
2038 		rect.bottom += borderWidth;
2039 
2040 		auto insideBorderRect = rect;
2041 
2042 		rect.left -= cs.paddingLeft;
2043 		rect.right += cs.paddingRight;
2044 		rect.top -= cs.paddingTop;
2045 		rect.bottom += cs.paddingBottom;
2046 
2047 		return rect;
2048 	}
2049 
2050 
2051 	// FIXME: I kinda want to hide events from implementation widgets
2052 	// so it just catches them all and stops propagation...
2053 	// i guess i can do it with a event listener on star.
2054 
2055 	mixin Emits!KeyDownEvent; ///
2056 	mixin Emits!KeyUpEvent; ///
2057 	mixin Emits!CharEvent; ///
2058 
2059 	mixin Emits!MouseDownEvent; ///
2060 	mixin Emits!MouseUpEvent; ///
2061 	mixin Emits!ClickEvent; ///
2062 	mixin Emits!DoubleClickEvent; ///
2063 	mixin Emits!MouseMoveEvent; ///
2064 	mixin Emits!MouseOverEvent; ///
2065 	mixin Emits!MouseOutEvent; ///
2066 	mixin Emits!MouseEnterEvent; ///
2067 	mixin Emits!MouseLeaveEvent; ///
2068 
2069 	mixin Emits!ResizeEvent; ///
2070 
2071 	mixin Emits!BlurEvent; ///
2072 	mixin Emits!FocusEvent; ///
2073 
2074 	mixin Emits!FocusInEvent; ///
2075 	mixin Emits!FocusOutEvent; ///
2076 }
2077 
2078 /+
2079 /++
2080 	Interface to indicate that the widget has a simple value property.
2081 
2082 	History:
2083 		Added August 26, 2021
2084 +/
2085 interface HasValue!T {
2086 	/// Getter
2087 	@property T value();
2088 	/// Setter
2089 	@property void value(T);
2090 }
2091 
2092 /++
2093 	Interface to indicate that the widget has a range of possible values for its simple value property.
2094 	This would be present on something like a slider or possibly a number picker.
2095 
2096 	History:
2097 		Added September 11, 2021
2098 +/
2099 interface HasRangeOfValues!T : HasValue!T {
2100 	/// The minimum and maximum values in the range, inclusive.
2101 	@property T minValue();
2102 	@property void minValue(T); /// ditto
2103 	@property T maxValue(); /// ditto
2104 	@property void maxValue(T); /// ditto
2105 
2106 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2107 	@property void step(T);
2108 	@property T step(); /// ditto
2109 }
2110 
2111 /++
2112 	Interface to indicate that the widget has a list of possible values the user can choose from.
2113 	This would be present on something like a drop-down selector.
2114 
2115 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2116 	combobox.
2117 
2118 	History:
2119 		Added September 11, 2021
2120 +/
2121 interface HasListOfValues!T : HasValue!T {
2122 	@property T[] values;
2123 	@property void values(T[]);
2124 
2125 	@property int selectedIndex(); // note it may return -1!
2126 	@property void selectedIndex(int);
2127 }
2128 +/
2129 
2130 /++
2131 	History:
2132 		Added September 2021 (dub v10.4)
2133 +/
2134 class GridLayout : Layout {
2135 
2136 	// 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.
2137 
2138 	/++
2139 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2140 	+/
2141 	enum Gravity {
2142 		Center    = 0,
2143 		NorthWest = North | West,
2144 		North     = 0b10_00,
2145 		NorthEast = North | East,
2146 		West      = 0b00_10,
2147 		East      = 0b00_01,
2148 		SouthWest = South | West,
2149 		South     = 0b01_00,
2150 		SouthEast = South | East,
2151 	}
2152 
2153 	/++
2154 		The width and height are in some proportional units and can often just be 12.
2155 	+/
2156 	this(int width, int height, Widget parent) {
2157 		this.gridWidth = width;
2158 		this.gridHeight = height;
2159 		super(parent);
2160 	}
2161 
2162 	/++
2163 		Sets the position of the given child.
2164 
2165 		The units of these arguments are in the proportional grid units you set in the constructor.
2166 	+/
2167 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2168 		// ensure it is in bounds
2169 		// then ensure no overlaps
2170 
2171 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2172 
2173 		foreach(ref position; positions) {
2174 			if(position.widget is child) {
2175 				position = p;
2176 				goto set;
2177 			}
2178 		}
2179 
2180 		positions ~= p;
2181 
2182 		set:
2183 
2184 		// FIXME: should this batch?
2185 		queueRecomputeChildLayout();
2186 
2187 		return child;
2188 	}
2189 
2190 	override void addChild(Widget w, int position = int.max) {
2191 		super.addChild(w, position);
2192 		//positions ~= ChildPosition(w);
2193 		if(position != int.max) {
2194 			// FIXME: align it so they actually match.
2195 		}
2196 	}
2197 
2198 	override void widgetRemoved(size_t idx, Widget w) {
2199 		// FIXME: keep the positions array aligned
2200 		// positions[idx].widget = null;
2201 	}
2202 
2203 	override void recomputeChildLayout() {
2204 		registerMovement();
2205 		int onGrid = cast(int) positions.length;
2206 		c: foreach(child; children) {
2207 			// just snap it to the grid
2208 			if(onGrid)
2209 			foreach(position; positions)
2210 				if(position.widget is child) {
2211 					child.x = this.width * position.x / this.gridWidth;
2212 					child.y = this.height * position.y / this.gridHeight;
2213 					child.width = this.width * position.width / this.gridWidth;
2214 					child.height = this.height * position.height / this.gridHeight;
2215 
2216 					auto diff = child.width - child.maxWidth();
2217 					// FIXME: gravity?
2218 					if(diff > 0) {
2219 						child.width = child.width - diff;
2220 
2221 						if(position.gravity & Gravity.West) {
2222 							// nothing needed, already aligned
2223 						} else if(position.gravity & Gravity.East) {
2224 							child.x += diff;
2225 						} else {
2226 							child.x += diff / 2;
2227 						}
2228 					}
2229 
2230 					diff = child.height - child.maxHeight();
2231 					// FIXME: gravity?
2232 					if(diff > 0) {
2233 						child.height = child.height - diff;
2234 
2235 						if(position.gravity & Gravity.North) {
2236 							// nothing needed, already aligned
2237 						} else if(position.gravity & Gravity.South) {
2238 							child.y += diff;
2239 						} else {
2240 							child.y += diff / 2;
2241 						}
2242 					}
2243 
2244 
2245 					child.recomputeChildLayout();
2246 					onGrid--;
2247 					continue c;
2248 				}
2249 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2250 		}
2251 	}
2252 
2253 	private struct ChildPosition {
2254 		Widget widget;
2255 		int x;
2256 		int y;
2257 		int width;
2258 		int height;
2259 		Gravity gravity;
2260 	}
2261 	private ChildPosition[] positions;
2262 
2263 	int gridWidth = 12;
2264 	int gridHeight = 12;
2265 }
2266 
2267 ///
2268 abstract class ComboboxBase : Widget {
2269 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2270 	// or to always show the list, we want CBS_SIMPLE == 1
2271 	version(win32_widgets)
2272 		this(uint style, Widget parent) {
2273 			super(parent);
2274 			createWin32Window(this, "ComboBox"w, null, style);
2275 		}
2276 	else version(custom_widgets)
2277 		this(Widget parent) {
2278 			super(parent);
2279 
2280 			addEventListener((KeyDownEvent event) {
2281 				if(event.key == Key.Up) {
2282 					if(selection_ > -1) { // -1 means select blank
2283 						selection_--;
2284 						fireChangeEvent();
2285 					}
2286 					event.preventDefault();
2287 				}
2288 				if(event.key == Key.Down) {
2289 					if(selection_ + 1 < options.length) {
2290 						selection_++;
2291 						fireChangeEvent();
2292 					}
2293 					event.preventDefault();
2294 				}
2295 
2296 			});
2297 
2298 		}
2299 	else static assert(false);
2300 
2301 	/++
2302 		Returns the current list of options in the selection.
2303 
2304 		History:
2305 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2306 	+/
2307 	final @property string[] options() const {
2308 		return cast(string[]) options_;
2309 	}
2310 
2311 	private string[] options_;
2312 	private int selection_ = -1;
2313 
2314 	/++
2315 		Adds an option to the end of options array.
2316 	+/
2317 	void addOption(string s) {
2318 		options_ ~= s;
2319 		version(win32_widgets)
2320 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2321 	}
2322 
2323 	/++
2324 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2325 	+/
2326 	int getSelection() {
2327 		return selection_;
2328 	}
2329 
2330 	/++
2331 		Returns the current selection as a string.
2332 
2333 		History:
2334 			Added November 17, 2021
2335 	+/
2336 	string getSelectionString() {
2337 		return selection_ == -1 ? null : options[selection_];
2338 	}
2339 
2340 	/++
2341 		Sets the current selection to an index in the options array, or to the given option if present.
2342 		Please note that the string version may do a linear lookup.
2343 
2344 		Returns:
2345 			the index you passed in
2346 
2347 		History:
2348 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2349 
2350 			The return value was `void` prior to March 1, 2022.
2351 	+/
2352 	int setSelection(int idx) {
2353 		selection_ = idx;
2354 		version(win32_widgets)
2355 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2356 
2357 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2358 		t.dispatch();
2359 
2360 		return idx;
2361 	}
2362 
2363 	/// ditto
2364 	int setSelection(string s) {
2365 		if(s !is null)
2366 		foreach(idx, item; options)
2367 			if(item == s) {
2368 				return setSelection(cast(int) idx);
2369 			}
2370 		return setSelection(-1);
2371 	}
2372 
2373 	/++
2374 		This event is fired when the selection changes. Note it inherits
2375 		from ChangeEvent!string, meaning you can use that as well, and it also
2376 		fills in [Event.intValue].
2377 	+/
2378 	static class SelectionChangedEvent : ChangeEvent!string {
2379 		this(Widget target, int iv, string sv) {
2380 			super(target, &stringValue);
2381 			this.iv = iv;
2382 			this.sv = sv;
2383 		}
2384 		immutable int iv;
2385 		immutable string sv;
2386 
2387 		override @property string stringValue() { return sv; }
2388 		override @property int intValue() { return iv; }
2389 	}
2390 
2391 	version(win32_widgets)
2392 	override void handleWmCommand(ushort cmd, ushort id) {
2393 		if(cmd == CBN_SELCHANGE) {
2394 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2395 			fireChangeEvent();
2396 		}
2397 	}
2398 
2399 	private void fireChangeEvent() {
2400 		if(selection_ >= options.length)
2401 			selection_ = -1;
2402 
2403 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2404 		t.dispatch();
2405 	}
2406 
2407 	version(win32_widgets) {
2408 		override int minHeight() { return defaultLineHeight + 6; }
2409 		override int maxHeight() { return defaultLineHeight + 6; }
2410 	} else {
2411 		override int minHeight() { return defaultLineHeight + 4; }
2412 		override int maxHeight() { return defaultLineHeight + 4; }
2413 	}
2414 
2415 	version(custom_widgets) {
2416 
2417 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2418 
2419 		SimpleWindow dropDown;
2420 		void popup() {
2421 			auto w = width;
2422 			// FIXME: suggestedDropdownHeight see below
2423 			auto h = cast(int) this.options.length * defaultLineHeight + 8;
2424 
2425 			auto coord = this.globalCoordinates();
2426 			auto dropDown = new SimpleWindow(
2427 				w, h,
2428 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null);
2429 
2430 			dropDown.move(coord.x, coord.y + this.height);
2431 
2432 			{
2433 				auto cs = getComputedStyle();
2434 				auto painter = dropDown.draw();
2435 				draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2436 				auto p = Point(4, 4);
2437 				painter.outlineColor = cs.foregroundColor;
2438 				foreach(option; options) {
2439 					painter.drawText(p, option);
2440 					p.y += defaultLineHeight;
2441 				}
2442 			}
2443 
2444 			dropDown.setEventHandlers(
2445 				(MouseEvent event) {
2446 					if(event.type == MouseEventType.buttonReleased) {
2447 						dropDown.close();
2448 						auto element = (event.y - 4) / defaultLineHeight;
2449 						if(element >= 0 && element <= options.length) {
2450 							selection_ = element;
2451 
2452 							fireChangeEvent();
2453 						}
2454 					}
2455 				}
2456 			);
2457 
2458 			dropDown.visibilityChanged = (bool visible) {
2459 				if(visible) {
2460 					this.redraw();
2461 					dropDown.grabInput();
2462 				} else {
2463 					dropDown.releaseInputGrab();
2464 				}
2465 			};
2466 
2467 			dropDown.show();
2468 		}
2469 
2470 	}
2471 }
2472 
2473 /++
2474 	A drop-down list where the user must select one of the
2475 	given options. Like `<select>` in HTML.
2476 +/
2477 class DropDownSelection : ComboboxBase {
2478 	this(Widget parent) {
2479 		version(win32_widgets)
2480 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
2481 		else version(custom_widgets) {
2482 			super(parent);
2483 
2484 			addEventListener("focus", () { this.redraw; });
2485 			addEventListener("blur", () { this.redraw; });
2486 			addEventListener(EventType.change, () { this.redraw; });
2487 			addEventListener("mousedown", () { this.focus(); this.popup(); });
2488 			addEventListener((KeyDownEvent event) {
2489 				if(event.key == Key.Space)
2490 					popup();
2491 			});
2492 		} else static assert(false);
2493 	}
2494 
2495 	mixin Padding!q{2};
2496 	static class Style : Widget.Style {
2497 		override FrameStyle borderStyle() { return FrameStyle.risen; }
2498 	}
2499 	mixin OverrideStyle!Style;
2500 
2501 	version(custom_widgets)
2502 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2503 		auto cs = getComputedStyle();
2504 
2505 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
2506 
2507 		painter.outlineColor = cs.foregroundColor;
2508 		painter.fillColor = cs.foregroundColor;
2509 
2510 		/+
2511 		Point[4] triangle;
2512 		enum padding = 6;
2513 		enum paddingV = 7;
2514 		enum triangleWidth = 10;
2515 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
2516 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
2517 		triangle[2] = Point(width - padding - 0, paddingV);
2518 		triangle[3] = triangle[0];
2519 		painter.drawPolygon(triangle[]);
2520 		+/
2521 
2522 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
2523 
2524 		painter.drawPolygon(
2525 			scaleWithDpi(Point(2, 6) + offset),
2526 			scaleWithDpi(Point(7, 11) + offset),
2527 			scaleWithDpi(Point(12, 6) + offset),
2528 			scaleWithDpi(Point(2, 6) + offset)
2529 		);
2530 
2531 
2532 		return bounds;
2533 	}
2534 
2535 	version(win32_widgets)
2536 	override void registerMovement() {
2537 		version(win32_widgets) {
2538 			if(hwnd) {
2539 				auto pos = getChildPositionRelativeToParentHwnd(this);
2540 				// the height given to this from Windows' perspective is supposed
2541 				// to include the drop down's height. so I add to it to give some
2542 				// room for that.
2543 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
2544 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
2545 			}
2546 		}
2547 		sendResizeEvent();
2548 	}
2549 }
2550 
2551 /++
2552 	A text box with a drop down arrow listing selections.
2553 	The user can choose from the list, or type their own.
2554 +/
2555 class FreeEntrySelection : ComboboxBase {
2556 	this(Widget parent) {
2557 		version(win32_widgets)
2558 			super(2 /* CBS_DROPDOWN */, parent);
2559 		else version(custom_widgets) {
2560 			super(parent);
2561 			auto hl = new HorizontalLayout(this);
2562 			lineEdit = new LineEdit(hl);
2563 
2564 			tabStop = false;
2565 
2566 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2567 
2568 			auto btn = new class ArrowButton {
2569 				this() {
2570 					super(ArrowDirection.down, hl);
2571 				}
2572 				override int maxHeight() {
2573 					return lineEdit.maxHeight;
2574 				}
2575 			};
2576 			//btn.addDirectEventListener("focus", &lineEdit.focus);
2577 			btn.addEventListener("triggered", &this.popup);
2578 			addEventListener(EventType.change, (Event event) {
2579 				lineEdit.content = event.stringValue;
2580 				lineEdit.focus();
2581 				redraw();
2582 			});
2583 		}
2584 		else static assert(false);
2585 	}
2586 
2587 	version(custom_widgets) {
2588 		LineEdit lineEdit;
2589 	}
2590 }
2591 
2592 /++
2593 	A combination of free entry with a list below it.
2594 +/
2595 class ComboBox : ComboboxBase {
2596 	this(Widget parent) {
2597 		version(win32_widgets)
2598 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
2599 		else version(custom_widgets) {
2600 			super(parent);
2601 			lineEdit = new LineEdit(this);
2602 			listWidget = new ListWidget(this);
2603 			listWidget.multiSelect = false;
2604 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
2605 				string c = null;
2606 				foreach(option; listWidget.options)
2607 					if(option.selected) {
2608 						c = option.label;
2609 						break;
2610 					}
2611 				lineEdit.content = c;
2612 			});
2613 
2614 			listWidget.tabStop = false;
2615 			this.tabStop = false;
2616 			listWidget.addEventListener("focus", &lineEdit.focus);
2617 			this.addEventListener("focus", &lineEdit.focus);
2618 
2619 			addDirectEventListener(EventType.change, {
2620 				listWidget.setSelection(selection_);
2621 				if(selection_ != -1)
2622 					lineEdit.content = options[selection_];
2623 				lineEdit.focus();
2624 				redraw();
2625 			});
2626 
2627 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2628 
2629 			listWidget.addDirectEventListener(EventType.change, {
2630 				int set = -1;
2631 				foreach(idx, opt; listWidget.options)
2632 					if(opt.selected) {
2633 						set = cast(int) idx;
2634 						break;
2635 					}
2636 				if(set != selection_)
2637 					this.setSelection(set);
2638 			});
2639 		} else static assert(false);
2640 	}
2641 
2642 	override int minHeight() { return defaultLineHeight * 3; }
2643 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
2644 	override int heightStretchiness() { return 5; }
2645 
2646 	version(custom_widgets) {
2647 		LineEdit lineEdit;
2648 		ListWidget listWidget;
2649 
2650 		override void addOption(string s) {
2651 			listWidget.options ~= ListWidget.Option(s);
2652 			ComboboxBase.addOption(s);
2653 		}
2654 	}
2655 }
2656 
2657 /+
2658 class Spinner : Widget {
2659 	version(win32_widgets)
2660 	this(Widget parent) {
2661 		super(parent);
2662 		parentWindow = parent.parentWindow;
2663 		auto hlayout = new HorizontalLayout(this);
2664 		lineEdit = new LineEdit(hlayout);
2665 		upDownControl = new UpDownControl(hlayout);
2666 	}
2667 
2668 	LineEdit lineEdit;
2669 	UpDownControl upDownControl;
2670 }
2671 
2672 class UpDownControl : Widget {
2673 	version(win32_widgets)
2674 	this(Widget parent) {
2675 		super(parent);
2676 		parentWindow = parent.parentWindow;
2677 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
2678 	}
2679 
2680 	override int minHeight() { return defaultLineHeight; }
2681 	override int maxHeight() { return defaultLineHeight * 3/2; }
2682 
2683 	override int minWidth() { return defaultLineHeight * 3/2; }
2684 	override int maxWidth() { return defaultLineHeight * 3/2; }
2685 }
2686 +/
2687 
2688 /+
2689 class DataView : Widget {
2690 	// this is the omnibus data viewer
2691 	// the internal data layout is something like:
2692 	// string[string][] but also each node can have parents
2693 }
2694 +/
2695 
2696 
2697 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
2698 
2699 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
2700 
2701 // FIXME: menus should prolly capture the mouse. ugh i kno.
2702 /*
2703 	TextEdit needs:
2704 
2705 	* caret manipulation
2706 	* selection control
2707 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
2708 
2709 	For example:
2710 
2711 	connect(paste, &textEdit.insertTextAtCaret);
2712 
2713 	would be nice.
2714 
2715 
2716 
2717 	I kinda want an omnibus dataview that combines list, tree,
2718 	and table - it can be switched dynamically between them.
2719 
2720 	Flattening policy: only show top level, show recursive, show grouped
2721 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
2722 
2723 	Single select, multi select, organization, drag+drop
2724 */
2725 
2726 //static if(UsingSimpledisplayX11)
2727 version(win32_widgets) {}
2728 else version(custom_widgets) {
2729 	enum scrollClickRepeatInterval = 50;
2730 
2731 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
2732 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
2733 	enum activeTabColor = lightAccentColor;
2734 	enum hoveringColor = Color(228, 228, 228);
2735 	enum buttonColor = windowBackgroundColor;
2736 	enum depressedButtonColor = darkAccentColor;
2737 	enum activeListXorColor = Color(255, 255, 127);
2738 	enum progressBarColor = Color(0, 0, 128);
2739 	enum activeMenuItemColor = Color(0, 0, 128);
2740 
2741 }}
2742 else static assert(false);
2743 deprecated("Get these properties off the `visualTheme` instead.") {
2744 	// these are used by horizontal rule so not just custom_widgets. for now at least.
2745 	enum darkAccentColor = Color(172, 172, 172);
2746 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
2747 }
2748 
2749 private const(wchar)* toWstringzInternal(in char[] s) {
2750 	wchar[] str;
2751 	str.reserve(s.length + 1);
2752 	foreach(dchar ch; s)
2753 		str ~= ch;
2754 	str ~= '\0';
2755 	return str.ptr;
2756 }
2757 
2758 static if(SimpledisplayTimerAvailable)
2759 void setClickRepeat(Widget w, int interval, int delay = 250) {
2760 	Timer timer;
2761 	int delayRemaining = delay / interval;
2762 	if(delayRemaining <= 1)
2763 		delayRemaining = 2;
2764 
2765 	immutable originalDelayRemaining = delayRemaining;
2766 
2767 	w.addDirectEventListener((scope MouseDownEvent ev) {
2768 		if(ev.srcElement !is w)
2769 			return;
2770 		if(timer !is null) {
2771 			timer.destroy();
2772 			timer = null;
2773 		}
2774 		delayRemaining = originalDelayRemaining;
2775 		timer = new Timer(interval, () {
2776 			if(delayRemaining > 0)
2777 				delayRemaining--;
2778 			else {
2779 				auto ev = new Event("triggered", w);
2780 				ev.sendDirectly();
2781 			}
2782 		});
2783 	});
2784 
2785 	w.addDirectEventListener((scope MouseUpEvent ev) {
2786 		if(ev.srcElement !is w)
2787 			return;
2788 		if(timer !is null) {
2789 			timer.destroy();
2790 			timer = null;
2791 		}
2792 	});
2793 
2794 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
2795 		if(ev.srcElement !is w)
2796 			return;
2797 		if(timer !is null) {
2798 			timer.destroy();
2799 			timer = null;
2800 		}
2801 	});
2802 
2803 }
2804 else
2805 void setClickRepeat(Widget w, int interval, int delay = 250) {}
2806 
2807 enum FrameStyle {
2808 	none, ///
2809 	risen, /// a 3d pop-out effect (think Windows 95 button)
2810 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
2811 	solid, ///
2812 	dotted, ///
2813 	fantasy, /// a style based on a popular fantasy video game
2814 }
2815 
2816 version(custom_widgets)
2817 deprecated
2818 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
2819 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2820 }
2821 
2822 version(custom_widgets)
2823 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
2824 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
2825 }
2826 
2827 version(custom_widgets)
2828 deprecated
2829 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
2830 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2831 }
2832 
2833 int getBorderWidth(FrameStyle style) {
2834 	final switch(style) {
2835 		case FrameStyle.sunk, FrameStyle.risen:
2836 			return 2;
2837 		case FrameStyle.none:
2838 			return 0;
2839 		case FrameStyle.solid:
2840 			return 1;
2841 		case FrameStyle.dotted:
2842 			return 1;
2843 		case FrameStyle.fantasy:
2844 			return 3;
2845 	}
2846 }
2847 
2848 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
2849 	int borderWidth = getBorderWidth(style);
2850 	final switch(style) {
2851 		case FrameStyle.sunk, FrameStyle.risen:
2852 			// outer layer
2853 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
2854 		break;
2855 		case FrameStyle.none:
2856 			painter.outlineColor = background;
2857 		break;
2858 		case FrameStyle.solid:
2859 			painter.pen = Pen(border, 1);
2860 		break;
2861 		case FrameStyle.dotted:
2862 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
2863 		break;
2864 		case FrameStyle.fantasy:
2865 			painter.pen = Pen(border, 3);
2866 		break;
2867 	}
2868 
2869 	painter.fillColor = background;
2870 	painter.drawRectangle(Point(x + 0, y + 0), width, height);
2871 
2872 
2873 	if(style == FrameStyle.sunk || style == FrameStyle.risen) {
2874 		// 3d effect
2875 		auto vt = WidgetPainter.visualTheme;
2876 
2877 		painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
2878 		painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
2879 		painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
2880 
2881 		// inner layer
2882 		//right, bottom
2883 		painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
2884 		painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
2885 		painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
2886 		// left, top
2887 		painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
2888 		painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
2889 		painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
2890 	} else if(style == FrameStyle.fantasy) {
2891 		painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
2892 		painter.fillColor = Color.transparent;
2893 		painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
2894 	}
2895 
2896 	return borderWidth;
2897 }
2898 
2899 /++
2900 	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.
2901 
2902 	See_Also:
2903 		[MenuItem]
2904 		[ToolButton]
2905 		[Menu.addItem]
2906 +/
2907 class Action {
2908 	version(win32_widgets) {
2909 		private int id;
2910 		private static int lastId = 9000;
2911 		private static Action[int] mapping;
2912 	}
2913 
2914 	KeyEvent accelerator;
2915 
2916 	// FIXME: disable message
2917 	// and toggle thing?
2918 	// ??? and trigger arguments too ???
2919 
2920 	/++
2921 		Params:
2922 			label = the textual label
2923 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
2924 			triggered = initial handler, more can be added via the [triggered] member.
2925 	+/
2926 	this(string label, ushort icon = 0, void delegate() triggered = null) {
2927 		this.label = label;
2928 		this.iconId = icon;
2929 		if(triggered !is null)
2930 			this.triggered ~= triggered;
2931 		version(win32_widgets) {
2932 			id = ++lastId;
2933 			mapping[id] = this;
2934 		}
2935 	}
2936 
2937 	private string label;
2938 	private ushort iconId;
2939 	// icon
2940 
2941 	// when it is triggered, the triggered event is fired on the window
2942 	/// The list of handlers when it is triggered.
2943 	void delegate()[] triggered;
2944 }
2945 
2946 /*
2947 	plan:
2948 		keyboard accelerators
2949 
2950 		* menus (and popups and tooltips)
2951 		* status bar
2952 		* toolbars and buttons
2953 
2954 		sortable table view
2955 
2956 		maybe notification area icons
2957 		basic clipboard
2958 
2959 		* radio box
2960 		splitter
2961 		toggle buttons (optionally mutually exclusive, like in Paint)
2962 		label, rich text display, multi line plain text (selectable)
2963 		* fieldset
2964 		* nestable grid layout
2965 		single line text input
2966 		* multi line text input
2967 		slider
2968 		spinner
2969 		list box
2970 		drop down
2971 		combo box
2972 		auto complete box
2973 		* progress bar
2974 
2975 		terminal window/widget (on unix it might even be a pty but really idk)
2976 
2977 		ok button
2978 		cancel button
2979 
2980 		keyboard hotkeys
2981 
2982 		scroll widget
2983 
2984 		event redirections and network transparency
2985 		script integration
2986 */
2987 
2988 
2989 /*
2990 	MENUS
2991 
2992 	auto bar = new MenuBar(window);
2993 	window.menuBar = bar;
2994 
2995 	auto fileMenu = bar.addItem(new Menu("&File"));
2996 	fileMenu.addItem(new MenuItem("&Exit"));
2997 
2998 
2999 	EVENTS
3000 
3001 	For controls, you should usually use "triggered" rather than "click", etc., because
3002 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3003 	This is the case on menus and pushbuttons.
3004 
3005 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3006 */
3007 
3008 
3009 /*
3010 enum LinePreference {
3011 	AlwaysOnOwnLine, // always on its own line
3012 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3013 	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
3014 }
3015 */
3016 
3017 /++
3018 	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.
3019 
3020 	---
3021 	class MyWidget : Widget {
3022 		this(Widget parent) { super(parent); }
3023 
3024 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3025 		mixin Padding!q{4};
3026 
3027 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3028 		mixin Margin!q{8};
3029 
3030 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3031 		// while Top/Bottom/Right remain 8 from the mixin above.
3032 		override int marginLeft() { return 2; }
3033 	}
3034 	---
3035 
3036 
3037 	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]).
3038 
3039 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3040 
3041 	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!).
3042 
3043 	* 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.
3044 +/
3045 mixin template Padding(string code) {
3046 	override int paddingLeft() { return mixin(code);}
3047 	override int paddingRight() { return mixin(code);}
3048 	override int paddingTop() { return mixin(code);}
3049 	override int paddingBottom() { return mixin(code);}
3050 }
3051 
3052 /// ditto
3053 mixin template Margin(string code) {
3054 	override int marginLeft() { return mixin(code);}
3055 	override int marginRight() { return mixin(code);}
3056 	override int marginTop() { return mixin(code);}
3057 	override int marginBottom() { return mixin(code);}
3058 }
3059 
3060 private
3061 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3062 	enum calcingV = relevantMeasure == "height";
3063 
3064 	parent.registerMovement();
3065 
3066 	if(parent.children.length == 0)
3067 		return;
3068 
3069 	auto parentStyle = parent.getComputedStyle();
3070 
3071 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3072 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3073 
3074 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3075 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3076 
3077 	// my own width and height should already be set by the caller of this function...
3078 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3079 		mixin("parentStyle.padding"~firstThingy~"()") -
3080 		mixin("parentStyle.padding"~secondThingy~"()");
3081 
3082 	int stretchinessSum;
3083 	int stretchyChildSum;
3084 	int lastMargin = 0;
3085 
3086 	int shrinkinessSum;
3087 	int shrinkyChildSum;
3088 
3089 	// set initial size
3090 	foreach(child; parent.children) {
3091 
3092 		auto childStyle = child.getComputedStyle();
3093 
3094 		if(cast(StaticPosition) child)
3095 			continue;
3096 		if(child.hidden)
3097 			continue;
3098 
3099 		const iw = child.flexBasisWidth();
3100 		const ih = child.flexBasisHeight();
3101 
3102 		static if(calcingV) {
3103 			child.width = parent.width -
3104 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3105 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3106 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3107 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3108 
3109 			if(child.width < 0)
3110 				child.width = 0;
3111 			if(child.width > childStyle.maxWidth())
3112 				child.width = childStyle.maxWidth();
3113 
3114 			if(iw > 0) {
3115 				auto totalPossible = child.width;
3116 				if(child.width > iw && child.widthStretchiness() == 0)
3117 					child.width = iw;
3118 			}
3119 
3120 			child.height = mymax(childStyle.minHeight(), ih);
3121 		} else {
3122 			// set to take all the space
3123 			child.height = parent.height -
3124 				mixin("childStyle.margin"~firstThingy~"()") -
3125 				mixin("childStyle.margin"~secondThingy~"()") -
3126 				mixin("parentStyle.padding"~firstThingy~"()") -
3127 				mixin("parentStyle.padding"~secondThingy~"()");
3128 
3129 			// then clamp it
3130 			if(child.height < 0)
3131 				child.height = 0;
3132 			if(child.height > childStyle.maxHeight())
3133 				child.height = childStyle.maxHeight();
3134 
3135 			// and if possible, respect the ideal target
3136 			if(ih > 0) {
3137 				auto totalPossible = child.height;
3138 				if(child.height > ih && child.heightStretchiness() == 0)
3139 					child.height = ih;
3140 			}
3141 
3142 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3143 			child.width = mymax(childStyle.minWidth(), iw);
3144 		}
3145 
3146 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3147 
3148 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3149 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3150 		lastMargin = margin;
3151 		spaceRemaining -= thisMargin + margin;
3152 
3153 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3154 		stretchinessSum += s;
3155 		if(s > 0)
3156 			stretchyChildSum++;
3157 
3158 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3159 		shrinkinessSum += s2;
3160 		if(s2 > 0)
3161 			shrinkyChildSum++;
3162 	}
3163 
3164 	if(spaceRemaining < 0 && shrinkyChildSum) {
3165 		// shrink to get into the space if it is possible
3166 		auto toRemove = -spaceRemaining;
3167 		auto removalPerItem  = toRemove * shrinkinessSum / shrinkyChildSum;
3168 		auto remainder = toRemove * shrinkinessSum % shrinkyChildSum;
3169 
3170 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3171 
3172 		foreach(child; parent.children) {
3173 			auto childStyle = child.getComputedStyle();
3174 			if(cast(StaticPosition) child)
3175 				continue;
3176 			if(child.hidden)
3177 				continue;
3178 			static if(calcingV) {
3179 				auto maximum = childStyle.maxHeight();
3180 			} else {
3181 				auto maximum = childStyle.maxWidth();
3182 			}
3183 
3184 			if(mixin("child._" ~ relevantMeasure) >= maximum)
3185 				continue;
3186 
3187 			mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh.
3188 
3189 			spaceRemaining += removalPerItem + remainder;
3190 		}
3191 	}
3192 
3193 	// stretch to fill space
3194 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3195 		auto spacePerChild = spaceRemaining / stretchinessSum;
3196 		bool spreadEvenly;
3197 		bool giveToBiggest;
3198 		if(spacePerChild <= 0) {
3199 			spacePerChild = spaceRemaining / stretchyChildSum;
3200 			spreadEvenly = true;
3201 		}
3202 		if(spacePerChild <= 0) {
3203 			giveToBiggest = true;
3204 		}
3205 		int previousSpaceRemaining = spaceRemaining;
3206 		stretchinessSum = 0;
3207 		Widget mostStretchy;
3208 		int mostStretchyS;
3209 		foreach(child; parent.children) {
3210 			auto childStyle = child.getComputedStyle();
3211 			if(cast(StaticPosition) child)
3212 				continue;
3213 			if(child.hidden)
3214 				continue;
3215 			static if(calcingV) {
3216 				auto maximum = childStyle.maxHeight();
3217 			} else {
3218 				auto maximum = childStyle.maxWidth();
3219 			}
3220 
3221 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3222 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3223 				mixin("child._" ~ relevantMeasure) -= adj;
3224 				spaceRemaining += adj;
3225 				continue;
3226 			}
3227 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3228 			if(s <= 0)
3229 				continue;
3230 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3231 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3232 			spaceRemaining -= spaceAdjustment;
3233 			if(mixin("child." ~ relevantMeasure) > maximum) {
3234 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3235 				mixin("child._" ~ relevantMeasure) -= diff;
3236 				spaceRemaining += diff;
3237 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3238 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3239 				if(mostStretchy is null || s >= mostStretchyS) {
3240 					mostStretchy = child;
3241 					mostStretchyS = s;
3242 				}
3243 			}
3244 		}
3245 
3246 		if(giveToBiggest && mostStretchy !is null) {
3247 			auto child = mostStretchy;
3248 			auto childStyle = child.getComputedStyle();
3249 			int spaceAdjustment = spaceRemaining;
3250 
3251 			static if(calcingV)
3252 				auto maximum = childStyle.maxHeight();
3253 			else
3254 				auto maximum = childStyle.maxWidth();
3255 
3256 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3257 			spaceRemaining -= spaceAdjustment;
3258 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3259 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3260 				mixin("child._" ~ relevantMeasure) -= diff;
3261 				spaceRemaining += diff;
3262 			}
3263 		}
3264 
3265 		if(spaceRemaining == previousSpaceRemaining) {
3266 			if(mostStretchy !is null) {
3267 				static if(calcingV)
3268 					auto maximum = mostStretchy.maxHeight();
3269 				else
3270 					auto maximum = mostStretchy.maxWidth();
3271 
3272 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3273 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3274 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3275 			}
3276 			break; // apparently nothing more we can do
3277 		}
3278 	}
3279 
3280 	foreach(child; parent.children) {
3281 		auto childStyle = child.getComputedStyle();
3282 		if(cast(StaticPosition) child)
3283 			continue;
3284 		if(child.hidden)
3285 			continue;
3286 
3287 		static if(calcingV)
3288 			auto maximum = childStyle.maxHeight();
3289 		else
3290 			auto maximum = childStyle.maxWidth();
3291 		if(mixin("child._" ~ relevantMeasure) > maximum)
3292 			mixin("child._" ~ relevantMeasure) = maximum;
3293 	}
3294 
3295 	// position
3296 	lastMargin = 0;
3297 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3298 	foreach(child; parent.children) {
3299 		auto childStyle = child.getComputedStyle();
3300 		if(cast(StaticPosition) child) {
3301 			child.recomputeChildLayout();
3302 			continue;
3303 		}
3304 		if(child.hidden)
3305 			continue;
3306 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3307 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3308 		currentPos += thisMargin;
3309 		static if(calcingV) {
3310 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3311 			child.y = currentPos;
3312 		} else {
3313 			child.x = currentPos;
3314 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3315 
3316 		}
3317 		currentPos += mixin("child." ~ relevantMeasure);
3318 		currentPos += margin;
3319 		lastMargin = margin;
3320 
3321 		child.recomputeChildLayout();
3322 	}
3323 }
3324 
3325 int mymax(int a, int b) { return a > b ? a : b; }
3326 int mymax(int a, int b, int c) {
3327 	auto d = mymax(a, b);
3328 	return c > d ? c : d;
3329 }
3330 
3331 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3332 // and here, it must be integrable with the layout, the event system, and not be painted over.
3333 version(win32_widgets) {
3334 
3335 	// this function just does stuff that a parent window needs for redirection
3336 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3337 		this_.hookedWndProc(msg, wParam, lParam);
3338 
3339 		switch(msg) {
3340 
3341 			case WM_VSCROLL, WM_HSCROLL:
3342 				auto pos = HIWORD(wParam);
3343 				auto m = LOWORD(wParam);
3344 
3345 				auto scrollbarHwnd = cast(HWND) lParam;
3346 
3347 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3348 
3349 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3350 
3351 					switch(m) {
3352 						/+
3353 						// I don't think those messages are ever actually sent normally by the widget itself,
3354 						// they are more used for the keyboard interface. methinks.
3355 						case SB_BOTTOM:
3356 							// writeln("end");
3357 							auto event = new Event("scrolltoend", *widgetp);
3358 							event.dispatch();
3359 							//if(!event.defaultPrevented)
3360 						break;
3361 						case SB_TOP:
3362 							// writeln("top");
3363 							auto event = new Event("scrolltobeginning", *widgetp);
3364 							event.dispatch();
3365 						break;
3366 						case SB_ENDSCROLL:
3367 							// idk
3368 						break;
3369 						+/
3370 						case SB_LINEDOWN:
3371 							(*widgetp).emitCommand!"scrolltonextline"();
3372 						return 0;
3373 						case SB_LINEUP:
3374 							(*widgetp).emitCommand!"scrolltopreviousline"();
3375 						return 0;
3376 						case SB_PAGEDOWN:
3377 							(*widgetp).emitCommand!"scrolltonextpage"();
3378 						return 0;
3379 						case SB_PAGEUP:
3380 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3381 						return 0;
3382 						case SB_THUMBPOSITION:
3383 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3384 							ev.dispatch();
3385 						return 0;
3386 						case SB_THUMBTRACK:
3387 							// eh kinda lying but i like the real time update display
3388 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3389 							ev.dispatch();
3390 
3391 							// the event loop doesn't seem to carry on with a requested redraw..
3392 							// so we request it to get our dirty bit set...
3393 							// then we need to immediately actually redraw it too for instant feedback to user
3394 							SimpleWindow.processAllCustomEvents();
3395 							SimpleWindow.processAllCustomEvents();
3396 							//if(this_.parentWindow)
3397 								//this_.parentWindow.actualRedraw();
3398 
3399 							// and this ensures the WM_PAINT message is sent fairly quickly
3400 							// still seems to lag a little in large windows but meh it basically works.
3401 							if(this_.parentWindow) {
3402 								// FIXME: if painting is slow, this does still lag
3403 								// we probably will want to expose some user hook to ScrollWindowEx
3404 								// or something.
3405 								UpdateWindow(this_.parentWindow.hwnd);
3406 							}
3407 						return 0;
3408 						default:
3409 					}
3410 				}
3411 			break;
3412 
3413 			case WM_CONTEXTMENU:
3414 				auto hwndFrom = cast(HWND) wParam;
3415 
3416 				auto xPos = cast(short) LOWORD(lParam);
3417 				auto yPos = cast(short) HIWORD(lParam);
3418 
3419 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3420 					POINT p;
3421 					p.x = xPos;
3422 					p.y = yPos;
3423 					ScreenToClient(hwnd, &p);
3424 					auto clientX = cast(ushort) p.x;
3425 					auto clientY = cast(ushort) p.y;
3426 
3427 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
3428 
3429 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
3430 						return 0;
3431 					}
3432 				}
3433 			break;
3434 
3435 			case WM_DRAWITEM:
3436 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
3437 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
3438 					return (*widgetp).handleWmDrawItem(dis);
3439 				}
3440 			break;
3441 
3442 			case WM_NOTIFY:
3443 				auto hdr = cast(NMHDR*) lParam;
3444 				auto hwndFrom = hdr.hwndFrom;
3445 				auto code = hdr.code;
3446 
3447 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3448 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
3449 				}
3450 			break;
3451 			case WM_COMMAND:
3452 				auto handle = cast(HWND) lParam;
3453 				auto cmd = HIWORD(wParam);
3454 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
3455 
3456 			default:
3457 				// pass it on
3458 		}
3459 		return 0;
3460 	}
3461 
3462 
3463 
3464 	extern(Windows)
3465 	private
3466 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
3467 	// but can i merge them?!
3468 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3469 		// try { writeln(iMessage); } catch(Exception e) {};
3470 
3471 		if(auto te = hWnd in Widget.nativeMapping) {
3472 			try {
3473 
3474 				te.hookedWndProc(iMessage, wParam, lParam);
3475 
3476 				int mustReturn;
3477 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
3478 				if(mustReturn)
3479 					return ret;
3480 
3481 				if(iMessage == WM_SETFOCUS) {
3482 					auto lol = *te;
3483 					while(lol !is null && lol.implicitlyCreated)
3484 						lol = lol.parent;
3485 					lol.focus();
3486 					//(*te).parentWindow.focusedWidget = lol;
3487 				}
3488 
3489 
3490 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
3491 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
3492 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
3493 						//GetStockObject(NULL_BRUSH);
3494 				}
3495 
3496 				auto pos = getChildPositionRelativeToParentOrigin(*te);
3497 				lastDefaultPrevented = false;
3498 				// try { writeln(typeid(*te)); } catch(Exception e) {}
3499 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
3500 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
3501 				else {
3502 					// it was something we recognized, should only call the window procedure if the default was not prevented
3503 				}
3504 			} catch(Exception e) {
3505 				assert(0, e.toString());
3506 			}
3507 			return 0;
3508 		}
3509 		assert(0, "shouldn't be receiving messages for this window....");
3510 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
3511 	}
3512 
3513 	extern(Windows)
3514 	private
3515 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
3516 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3517 		if(iMessage == WM_ERASEBKGND) {
3518 			auto dc = GetDC(hWnd);
3519 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
3520 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
3521 			RECT r;
3522 			GetWindowRect(hWnd, &r);
3523 			// since the pen is null, to fill the whole space, we need the +1 on both.
3524 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
3525 			SelectObject(dc, p);
3526 			SelectObject(dc, b);
3527 			ReleaseDC(hWnd, dc);
3528 			InvalidateRect(hWnd, null, false); // redraw the border
3529 			return 1;
3530 		}
3531 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
3532 	}
3533 
3534 	/++
3535 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
3536 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
3537 		of minigui's expectations.
3538 
3539 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
3540 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
3541 
3542 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
3543 
3544 		To check if you can use this, use `static if(UsingWin32Widgets)`.
3545 	+/
3546 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
3547 		assert(p.parentWindow !is null);
3548 		assert(p.parentWindow.win.impl.hwnd !is null);
3549 
3550 		auto bsgroupbox = style == BS_GROUPBOX;
3551 
3552 		HWND phwnd;
3553 
3554 		auto wtf = p.parent;
3555 		while(wtf) {
3556 			if(wtf.hwnd !is null) {
3557 				phwnd = wtf.hwnd;
3558 				break;
3559 			}
3560 			wtf = wtf.parent;
3561 		}
3562 
3563 		if(phwnd is null)
3564 			phwnd = p.parentWindow.win.impl.hwnd;
3565 
3566 		assert(phwnd !is null);
3567 
3568 		WCharzBuffer wt = WCharzBuffer(windowText);
3569 
3570 		style |= WS_VISIBLE | WS_CHILD;
3571 		//if(className != WC_TABCONTROL)
3572 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
3573 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
3574 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
3575 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
3576 
3577 		assert(p.hwnd !is null);
3578 
3579 
3580 		static HFONT font;
3581 		if(font is null) {
3582 			NONCLIENTMETRICS params;
3583 			params.cbSize = params.sizeof;
3584 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
3585 				font = CreateFontIndirect(&params.lfMessageFont);
3586 			}
3587 		}
3588 
3589 		if(font)
3590 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
3591 
3592 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
3593 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
3594 		Widget.nativeMapping[p.hwnd] = p;
3595 
3596 		if(bsgroupbox)
3597 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
3598 		else
3599 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3600 
3601 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
3602 
3603 		p.registerMovement();
3604 	}
3605 }
3606 
3607 version(win32_widgets)
3608 private
3609 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
3610 	if(hwnd is null || hwnd in Widget.nativeMapping)
3611 		return true;
3612 	auto parent = cast(Widget) cast(void*) lparam;
3613 	Widget p = new Widget(null);
3614 	p._parent = parent;
3615 	p.parentWindow = parent.parentWindow;
3616 	p.hwnd = hwnd;
3617 	p.implicitlyCreated = true;
3618 	Widget.nativeMapping[p.hwnd] = p;
3619 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3620 	return true;
3621 }
3622 
3623 /++
3624 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
3625 +/
3626 struct WidgetPainter {
3627 	this(ScreenPainter screenPainter, Widget drawingUpon) {
3628 		this.drawingUpon = drawingUpon;
3629 		this.screenPainter = screenPainter;
3630 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3631 			this.screenPainter.setFont(font);
3632 	}
3633 
3634 	/++
3635 		EXPERIMENTAL. subject to change.
3636 
3637 		When you draw a cursor, you can draw this to notify your window of where it is,
3638 		for IME systems to use.
3639 	+/
3640 	void notifyCursorPosition(int x, int y, int width, int height) {
3641 		if(auto a = drawingUpon.parentWindow)
3642 		if(auto w = a.inputProxy) {
3643 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
3644 		}
3645 	}
3646 
3647 
3648 	///
3649 	ScreenPainter screenPainter;
3650 	/// Forward to the screen painter for other methods
3651 	alias screenPainter this;
3652 
3653 	private Widget drawingUpon;
3654 
3655 	/++
3656 		This is the list of rectangles that actually need to be redrawn.
3657 
3658 		Not actually implemented yet.
3659 	+/
3660 	Rectangle[] invalidatedRectangles;
3661 
3662 	private static BaseVisualTheme _visualTheme;
3663 
3664 	/++
3665 		Functions to access the visual theme and helpers to easily use it.
3666 
3667 		These are aware of the current widget's computed style out of the theme.
3668 	+/
3669 	static @property BaseVisualTheme visualTheme() {
3670 		if(_visualTheme is null)
3671 			_visualTheme = new DefaultVisualTheme();
3672 		return _visualTheme;
3673 	}
3674 
3675 	/// ditto
3676 	static @property void visualTheme(BaseVisualTheme theme) {
3677 		_visualTheme = theme;
3678 
3679 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
3680 	}
3681 
3682 	/// ditto
3683 	Color themeForeground() {
3684 		return drawingUpon.getComputedStyle().foregroundColor();
3685 	}
3686 
3687 	/// ditto
3688 	Color themeBackground() {
3689 		return drawingUpon.getComputedStyle().background.color;
3690 	}
3691 
3692 	int isDarkTheme() {
3693 		return 0; // unspecified, yes, no as enum. FIXME
3694 	}
3695 
3696 	/++
3697 		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.
3698 
3699 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
3700 
3701 		If you change teh clip rectangle, you should change it back before you return.
3702 
3703 
3704 		The sequence it uses is:
3705 			background
3706 			content (delegated to you)
3707 			border
3708 			focused outline
3709 			selected overlay
3710 
3711 		Example code:
3712 
3713 		---
3714 		void paint(WidgetPainter painter) {
3715 			painter.drawThemed((bounds) {
3716 				return bounds; // if the selection overlay should be contained, you can return it here.
3717 			});
3718 		}
3719 		---
3720 	+/
3721 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
3722 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
3723 			return drawBody(bounds);
3724 		});
3725 	}
3726 	// this overload is actually mroe for setting the delegate to a virtual function
3727 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
3728 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
3729 
3730 		auto cs = drawingUpon.getComputedStyle();
3731 
3732 		auto bg = cs.background.color;
3733 
3734 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
3735 
3736 		rect.left += borderWidth;
3737 		rect.right -= borderWidth;
3738 		rect.top += borderWidth;
3739 		rect.bottom -= borderWidth;
3740 
3741 		auto insideBorderRect = rect;
3742 
3743 		rect.left += cs.paddingLeft;
3744 		rect.right -= cs.paddingRight;
3745 		rect.top += cs.paddingTop;
3746 		rect.bottom -= cs.paddingBottom;
3747 
3748 		this.outlineColor = this.themeForeground;
3749 		this.fillColor = bg;
3750 
3751 		auto widgetFont = cs.fontCached;
3752 		if(widgetFont !is null)
3753 			this.setFont(widgetFont);
3754 
3755 		rect = drawBody(this, rect);
3756 
3757 		if(widgetFont !is null) {
3758 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3759 				this.setFont(vtFont);
3760 			else
3761 				this.setFont(null);
3762 		}
3763 
3764 		if(auto os = cs.outlineStyle()) {
3765 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
3766 			this.fillColor = Color.transparent;
3767 			this.drawRectangle(insideBorderRect);
3768 		}
3769 	}
3770 
3771 	/++
3772 		First, draw the background.
3773 		Then draw your content.
3774 		Next, draw the border.
3775 		And the focused indicator.
3776 		And the is-selected box.
3777 
3778 		If it is focused i can draw the outline too...
3779 
3780 		If selected i can even do the xor action but that's at the end.
3781 	+/
3782 	void drawThemeBackground() {
3783 
3784 	}
3785 
3786 	void drawThemeBorder() {
3787 
3788 	}
3789 
3790 	// all this stuff is a dangerous experiment....
3791 	static class ScriptableVersion {
3792 		ScreenPainterImplementation* p;
3793 		int originX, originY;
3794 
3795 		@scriptable:
3796 		void drawRectangle(int x, int y, int width, int height) {
3797 			p.drawRectangle(x + originX, y + originY, width, height);
3798 		}
3799 		void drawLine(int x1, int y1, int x2, int y2) {
3800 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
3801 		}
3802 		void drawText(int x, int y, string text) {
3803 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
3804 		}
3805 		void setOutlineColor(int r, int g, int b) {
3806 			p.pen = Pen(Color(r,g,b), 1);
3807 		}
3808 		void setFillColor(int r, int g, int b) {
3809 			p.fillColor = Color(r,g,b);
3810 		}
3811 	}
3812 
3813 	ScriptableVersion toArsdJsvar() {
3814 		auto sv = new ScriptableVersion;
3815 		sv.p = this.screenPainter.impl;
3816 		sv.originX = this.screenPainter.originX;
3817 		sv.originY = this.screenPainter.originY;
3818 		return sv;
3819 	}
3820 
3821 	static WidgetPainter fromJsVar(T)(T t) {
3822 		return WidgetPainter.init;
3823 	}
3824 	// done..........
3825 }
3826 
3827 
3828 struct Style {
3829 	static struct helper(string m, T) {
3830 		enum method = m;
3831 		T v;
3832 
3833 		mixin template MethodOverride(typeof(this) v) {
3834 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
3835 		}
3836 	}
3837 
3838 	static auto opDispatch(string method, T)(T value) {
3839 		return helper!(method, T)(value);
3840 	}
3841 }
3842 
3843 /++
3844 	Implementation detail of the [ControlledBy] UDA.
3845 
3846 	History:
3847 		Added Oct 28, 2020
3848 +/
3849 struct ControlledBy_(T, Args...) {
3850 	Args args;
3851 
3852 	static if(Args.length)
3853 	this(Args args) {
3854 		this.args = args;
3855 	}
3856 
3857 	private T construct(Widget parent) {
3858 		return new T(args, parent);
3859 	}
3860 }
3861 
3862 /++
3863 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
3864 
3865 	History:
3866 		Added Oct 28, 2020
3867 +/
3868 auto ControlledBy(T, Args...)(Args args) {
3869 	return ControlledBy_!(T, Args)(args);
3870 }
3871 
3872 struct ContainerMeta {
3873 	string name;
3874 	ContainerMeta[] children;
3875 	Widget function(Widget parent) factory;
3876 
3877 	Widget instantiate(Widget parent) {
3878 		auto n = factory(parent);
3879 		n.name = name;
3880 		foreach(child; children)
3881 			child.instantiate(n);
3882 		return n;
3883 	}
3884 }
3885 
3886 /++
3887 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
3888 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
3889 
3890 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
3891 	structures. It works fine on structs declared inside functions though.
3892 
3893 	See: https://issues.dlang.org/show_bug.cgi?id=21984
3894 +/
3895 template Container(CArgs...) {
3896 	static if(CArgs.length && is(CArgs[0] : Widget)) {
3897 		private alias Super = CArgs[0];
3898 		private alias CArgs2 = CArgs[1 .. $];
3899 	} else {
3900 		private alias Super = Layout;
3901 		private alias CArgs2 = CArgs;
3902 	}
3903 
3904 	class Container : Super {
3905 		this(Widget parent) { super(parent); }
3906 
3907 		// just to partially support old gdc versions
3908 		version(GNU) {
3909 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
3910 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
3911 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
3912 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
3913 		} else mixin(q{
3914 			static foreach(Arg; CArgs2) {
3915 				mixin Arg.MethodOverride!(Arg);
3916 			}
3917 		});
3918 
3919 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
3920 			return ContainerMeta(
3921 				name,
3922 				children.dup,
3923 				function (Widget parent) { return new typeof(this)(parent); }
3924 			);
3925 		}
3926 
3927 		static ContainerMeta opCall(ContainerMeta[] children...) {
3928 			return opCall(null, children);
3929 		}
3930 	}
3931 }
3932 
3933 /++
3934 	The data controller widget is created by reflecting over the given
3935 	data type. You can use [ControlledBy] as a UDA on a struct or
3936 	just let it create things automatically.
3937 
3938 	Unlike [dialog], this uses real-time updating of the data and
3939 	you add it to another window yourself.
3940 
3941 	---
3942 		struct Test {
3943 			int x;
3944 			int y;
3945 		}
3946 
3947 		auto window = new Window();
3948 		auto dcw = new DataControllerWidget!Test(new Test, window);
3949 	---
3950 
3951 	The way it works is any public members are given a widget based
3952 	on their data type, and public methods trigger an action button
3953 	if no relevant parameters or a dialog action if it does have
3954 	parameters, similar to the [menu] facility.
3955 
3956 	If you change data programmatically, without going through the
3957 	DataControllerWidget methods, you will have to tell it something
3958 	has changed and it needs to redraw. This is done with the `invalidate`
3959 	method.
3960 
3961 	History:
3962 		Added Oct 28, 2020
3963 +/
3964 /// Group: generating_from_code
3965 class DataControllerWidget(T) : WidgetContainer {
3966 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
3967 		private alias Tref = T;
3968 	else
3969 		private alias Tref = T*;
3970 
3971 	Tref datum;
3972 
3973 	/++
3974 		See_also: [addDataControllerWidget]
3975 	+/
3976 	this(Tref datum, Widget parent) {
3977 		this.datum = datum;
3978 
3979 		Widget cp = this;
3980 
3981 		super(parent);
3982 
3983 		foreach(attr; __traits(getAttributes, T))
3984 			static if(is(typeof(attr) == ContainerMeta)) {
3985 				cp = attr.instantiate(this);
3986 			}
3987 
3988 		auto def = this.getByName("default");
3989 		if(def !is null)
3990 			cp = def;
3991 
3992 		Widget helper(string name) {
3993 			auto maybe = this.getByName(name);
3994 			if(maybe is null)
3995 				return cp;
3996 			return maybe;
3997 
3998 		}
3999 
4000 		foreach(member; __traits(allMembers, T))
4001 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4002 		static if(is(typeof(__traits(getMember, this.datum, member))))
4003 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4004 			void delegate() update;
4005 
4006 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4007 
4008 			if(update)
4009 				updaters ~= update;
4010 
4011 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4012 				w.addEventListener("triggered", delegate() {
4013 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))();
4014 					notifyDataUpdated();
4015 				});
4016 			} else static if(is(typeof(w.isChecked) == bool)) {
4017 				w.addEventListener(EventType.change, (Event ev) {
4018 					__traits(getMember, this.datum, member) = w.isChecked;
4019 				});
4020 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4021 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4022 			} else static if(is(typeof(w.value) == int)) {
4023 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4024 			} else static if(is(typeof(w) == DropDownSelection)) {
4025 				// special case for this to kinda support enums and such. coudl be better though
4026 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4027 			} else {
4028 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4029 			}
4030 		}
4031 	}
4032 
4033 	/++
4034 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4035 
4036 		History:
4037 			Added May 28, 2021
4038 	+/
4039 	void notifyDataUpdated() {
4040 		foreach(updater; updaters)
4041 			updater();
4042 
4043 		this.emit!(ChangeEvent!void)(delegate{});
4044 	}
4045 
4046 	private Widget[string] memberWidgets;
4047 	private void delegate()[] updaters;
4048 
4049 	mixin Emits!(ChangeEvent!void);
4050 }
4051 
4052 private int saturatedSum(int[] values...) {
4053 	int sum;
4054 	foreach(value; values) {
4055 		if(value == int.max)
4056 			return int.max;
4057 		sum += value;
4058 	}
4059 	return sum;
4060 }
4061 
4062 void genericSetValue(T, W)(T* where, W what) {
4063 	import std.conv;
4064 	*where = to!T(what);
4065 	//*where = cast(T) stringToLong(what);
4066 }
4067 
4068 /++
4069 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4070 
4071 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4072 
4073 	Note that this creates the widget but does not attach any event handlers to it.
4074 +/
4075 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4076 
4077 	string displayName = __traits(identifier, tt).beautify;
4078 
4079 	static if(controlledByCount!tt == 1) {
4080 		foreach(i, attr; __traits(getAttributes, tt)) {
4081 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4082 				auto w = attr.construct(parent);
4083 				static if(__traits(compiles, w.setPosition(*valptr)))
4084 					update = () { w.setPosition(*valptr); };
4085 				else static if(__traits(compiles, w.setValue(*valptr)))
4086 					update = () { w.setValue(*valptr); };
4087 
4088 				if(update)
4089 					update();
4090 				return w;
4091 			}
4092 		}
4093 	} else static if(controlledByCount!tt == 0) {
4094 		static if(is(typeof(tt) == enum)) {
4095 			// FIXME: update
4096 			auto dds = new DropDownSelection(parent);
4097 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4098 				dds.addOption(option);
4099 				if(__traits(getMember, typeof(tt), option) == *valptr)
4100 					dds.setSelection(cast(int) idx);
4101 			}
4102 			return dds;
4103 		} else static if(is(typeof(tt) == bool)) {
4104 			auto box = new Checkbox(displayName, parent);
4105 			update = () { box.isChecked = *valptr; };
4106 			update();
4107 			return box;
4108 		} else static if(is(typeof(tt) : const long)) {
4109 			auto le = new LabeledLineEdit(displayName, parent);
4110 			update = () { le.content = toInternal!string(*valptr); };
4111 			update();
4112 			return le;
4113 		} else static if(is(typeof(tt) : const double)) {
4114 			auto le = new LabeledLineEdit(displayName, parent);
4115 			import std.conv;
4116 			update = () { le.content = to!string(*valptr); };
4117 			update();
4118 			return le;
4119 		} else static if(is(typeof(tt) : const string)) {
4120 			auto le = new LabeledLineEdit(displayName, parent);
4121 			update = () { le.content = *valptr; };
4122 			update();
4123 			return le;
4124 		} else static if(is(typeof(tt) == function)) {
4125 			auto w = new Button(displayName, parent);
4126 			return w;
4127 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4128 			return parent.addDataControllerWidget(tt);
4129 		} else static assert(0, typeof(tt).stringof);
4130 	} else static assert(0, "multiple controllers not yet supported");
4131 }
4132 
4133 private template controlledByCount(alias tt) {
4134 	static int helper() {
4135 		int count;
4136 		foreach(i, attr; __traits(getAttributes, tt))
4137 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4138 				count++;
4139 		return count;
4140 	}
4141 
4142 	enum controlledByCount = helper;
4143 }
4144 
4145 /++
4146 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4147 
4148 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4149 
4150 	History:
4151 		The `redrawOnChange` parameter was added on May 28, 2021.
4152 +/
4153 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4154 	auto dcw = new DataControllerWidget!T(t, parent);
4155 	initializeDataControllerWidget(dcw, redrawOnChange);
4156 	return dcw;
4157 }
4158 
4159 /// ditto
4160 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4161 	auto dcw = new DataControllerWidget!T(t, parent);
4162 	initializeDataControllerWidget(dcw, redrawOnChange);
4163 	return dcw;
4164 }
4165 
4166 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4167 	if(redrawOnChange !is null)
4168 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4169 }
4170 
4171 /++
4172 	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.
4173 
4174 	History:
4175 		Finalized on June 3, 2021 for the dub v10.0 release
4176 +/
4177 struct StyleInformation {
4178 	private Widget w;
4179 	private BaseVisualTheme visualTheme;
4180 
4181 	private this(Widget w) {
4182 		this.w = w;
4183 		this.visualTheme = WidgetPainter.visualTheme;
4184 	}
4185 
4186 	/++
4187 		Forwards to [Widget.Style]
4188 
4189 		Bugs:
4190 			It is supposed to fall back to the [VisualTheme] if
4191 			the style doesn't override the default, but that is
4192 			not generally implemented. Many of them may end up
4193 			being explicit overloads instead of the generic
4194 			opDispatch fallback, like [font] is now.
4195 	+/
4196 	public @property opDispatch(string name)() {
4197 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4198 		w.useStyleProperties((scope Widget.Style props) {
4199 		//visualTheme.useStyleProperties(w, (props) {
4200 			prop = __traits(getMember, props, name);
4201 		});
4202 		return prop;
4203 	}
4204 
4205 	/++
4206 		Returns the cached font object associated with the widget,
4207 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4208 
4209 		History:
4210 			Prior to March 21, 2022 (dub v10.7), `font` went through
4211 			[opDispatch], which did not use the cache. You can now call it
4212 			repeatedly without guilt.
4213 	+/
4214 	public @property OperatingSystemFont font() {
4215 		OperatingSystemFont prop;
4216 		w.useStyleProperties((scope Widget.Style props) {
4217 			prop = props.fontCached;
4218 		});
4219 		if(prop is null) {
4220 			prop = visualTheme.defaultFontCached(w.currentDpi);
4221 		}
4222 		return prop;
4223 	}
4224 
4225 	@property {
4226 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4227 		/** */ int paddingLeft() { return w.paddingLeft(); }
4228 		/** */ int paddingRight() { return w.paddingRight(); }
4229 		/** */ int paddingTop() { return w.paddingTop(); }
4230 		/** */ int paddingBottom() { return w.paddingBottom(); }
4231 
4232 		/** */ int marginLeft() { return w.marginLeft(); }
4233 		/** */ int marginRight() { return w.marginRight(); }
4234 		/** */ int marginTop() { return w.marginTop(); }
4235 		/** */ int marginBottom() { return w.marginBottom(); }
4236 
4237 		/** */ int maxHeight() { return w.maxHeight(); }
4238 		/** */ int minHeight() { return w.minHeight(); }
4239 
4240 		/** */ int maxWidth() { return w.maxWidth(); }
4241 		/** */ int minWidth() { return w.minWidth(); }
4242 
4243 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4244 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4245 
4246 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4247 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4248 
4249 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4250 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4251 
4252 		// Global helpers some of these are unstable.
4253 		static:
4254 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4255 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4256 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4257 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4258 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4259 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4260 
4261 		/** */ Color activeTabColor() { return lightAccentColor; }
4262 		/** */ Color buttonColor() { return windowBackgroundColor; }
4263 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4264 		/** */ Color hoveringColor() { return lightAccentColor; }
4265 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4266 			auto c = WidgetPainter.visualTheme.selectionColor();
4267 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4268 		}
4269 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4270 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4271 	}
4272 
4273 
4274 
4275 	/+
4276 
4277 	private static auto extractStyleProperty(string name)(Widget w) {
4278 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4279 		w.useStyleProperties((props) {
4280 			prop = __traits(getMember, props, name);
4281 		});
4282 		return prop;
4283 	}
4284 
4285 	// FIXME: clear this upon a X server disconnect
4286 	private static OperatingSystemFont[string] fontCache;
4287 
4288 	T getProperty(T)(string name, lazy T default_) {
4289 		if(visualTheme !is null) {
4290 			auto str = visualTheme.getPropertyString(w, name);
4291 			if(str is null)
4292 				return default_;
4293 			static if(is(T == Color))
4294 				return Color.fromString(str);
4295 			else static if(is(T == Measurement))
4296 				return Measurement(cast(int) toInternal!int(str));
4297 			else static if(is(T == WidgetBackground))
4298 				return WidgetBackground.fromString(str);
4299 			else static if(is(T == OperatingSystemFont)) {
4300 				if(auto f = str in fontCache)
4301 					return *f;
4302 				else
4303 					return fontCache[str] = new OperatingSystemFont(str);
4304 			} else static if(is(T == FrameStyle)) {
4305 				switch(str) {
4306 					default:
4307 						return FrameStyle.none;
4308 					foreach(style; __traits(allMembers, FrameStyle))
4309 					case style:
4310 						return __traits(getMember, FrameStyle, style);
4311 				}
4312 			} else static assert(0);
4313 		} else
4314 			return default_;
4315 	}
4316 
4317 	static struct Measurement {
4318 		int value;
4319 		alias value this;
4320 	}
4321 
4322 	@property:
4323 
4324 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4325 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4326 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4327 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4328 
4329 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4330 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4331 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4332 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4333 
4334 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4335 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4336 
4337 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4338 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4339 
4340 
4341 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4342 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4343 
4344 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4345 
4346 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4347 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4348 
4349 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4350 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4351 
4352 
4353 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4354 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4355 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4356 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4357 
4358 	Color activeTabColor() { return lightAccentColor; }
4359 	Color buttonColor() { return windowBackgroundColor; }
4360 	Color depressedButtonColor() { return darkAccentColor; }
4361 	Color hoveringColor() { return Color(228, 228, 228); }
4362 	Color activeListXorColor() {
4363 		auto c = WidgetPainter.visualTheme.selectionColor();
4364 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4365 	}
4366 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4367 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4368 	+/
4369 }
4370 
4371 
4372 
4373 // pragma(msg, __traits(classInstanceSize, Widget));
4374 
4375 /*private*/ template EventString(E) {
4376 	static if(is(typeof(E.EventString)))
4377 		enum EventString = E.EventString;
4378 	else
4379 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4380 }
4381 
4382 /*private*/ template EventStringIdentifier(E) {
4383 	string helper() {
4384 		auto es = EventString!E;
4385 		char[] id = new char[](es.length * 2);
4386 		size_t idx;
4387 		foreach(char ch; es) {
4388 			id[idx++] = cast(char)('a' + (ch >> 4));
4389 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4390 		}
4391 		return cast(string) id;
4392 	}
4393 
4394 	enum EventStringIdentifier = helper();
4395 }
4396 
4397 
4398 template classStaticallyEmits(This, EventType) {
4399 	static if(is(This Base == super))
4400 		static if(is(Base : Widget))
4401 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4402 		else
4403 			enum baseEmits = false;
4404 	else
4405 		enum baseEmits = false;
4406 
4407 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
4408 
4409 	enum classStaticallyEmits = thisEmits || baseEmits;
4410 }
4411 
4412 /++
4413 	A helper to make widgets out of other native windows.
4414 
4415 	History:
4416 		Factored out of OpenGlWidget on November 5, 2021
4417 +/
4418 class NestedChildWindowWidget : Widget {
4419 	SimpleWindow win;
4420 
4421 	/++
4422 		Used on X to send focus to the appropriate child window when requested by the window manager.
4423 
4424 		Normally returns its own nested window. Can also return another child or null to revert to the parent
4425 		if you override it in a child class.
4426 
4427 		History:
4428 			Added April 2, 2022 (dub v10.8)
4429 	+/
4430 	SimpleWindow focusableWindow() {
4431 		return win;
4432 	}
4433 
4434 	///
4435 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4436 	this(SimpleWindow win, Widget parent) {
4437 		this.parentWindow = parent.parentWindow;
4438 		this.win = win;
4439 
4440 		super(parent);
4441 		windowsetup(win);
4442 	}
4443 
4444 	static protected SimpleWindow getParentWindow(Widget parent) {
4445 		assert(parent !is null);
4446 		SimpleWindow pwin = parent.parentWindow.win;
4447 
4448 		version(win32_widgets) {
4449 			HWND phwnd;
4450 			auto wtf = parent;
4451 			while(wtf) {
4452 				if(wtf.hwnd) {
4453 					phwnd = wtf.hwnd;
4454 					break;
4455 				}
4456 				wtf = wtf.parent;
4457 			}
4458 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
4459 			if(phwnd)
4460 				pwin = new SimpleWindow(phwnd);
4461 		}
4462 
4463 		return pwin;
4464 	}
4465 
4466 	/++
4467 		Called upon the nested window being destroyed.
4468 		Remember the window has already been destroyed at
4469 		this point, so don't use the native handle for anything.
4470 
4471 		History:
4472 			Added April 3, 2022 (dub v10.8)
4473 	+/
4474 	protected void dispose() {
4475 
4476 	}
4477 
4478 	protected void windowsetup(SimpleWindow w) {
4479 		/*
4480 		win.onFocusChange = (bool getting) {
4481 			if(getting)
4482 				this.focus();
4483 		};
4484 		*/
4485 
4486 		/+
4487 		win.onFocusChange = (bool getting) {
4488 			if(getting) {
4489 				this.parentWindow.focusedWidget = this;
4490 				this.emit!FocusEvent();
4491 				this.emit!FocusInEvent();
4492 			} else {
4493 				this.emit!BlurEvent();
4494 				this.emit!FocusOutEvent();
4495 			}
4496 		};
4497 		+/
4498 
4499 		win.onDestroyed = () {
4500 			this.dispose();
4501 		};
4502 
4503 		version(win32_widgets) {
4504 			Widget.nativeMapping[win.hwnd] = this;
4505 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4506 		} else {
4507 			win.setEventHandlers(
4508 				(MouseEvent e) {
4509 					Widget p = this;
4510 					while(p ! is parentWindow) {
4511 						e.x += p.x;
4512 						e.y += p.y;
4513 						p = p.parent;
4514 					}
4515 					parentWindow.dispatchMouseEvent(e);
4516 				},
4517 				(KeyEvent e) {
4518 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
4519 					parentWindow.dispatchKeyEvent(e);
4520 				},
4521 				(dchar e) {
4522 					parentWindow.dispatchCharEvent(e);
4523 				},
4524 			);
4525 		}
4526 
4527 	}
4528 
4529 	override void showing(bool s, bool recalc) {
4530 		auto cur = hidden;
4531 		win.hidden = !s;
4532 		if(cur != s && s)
4533 			redraw();
4534 	}
4535 
4536 	/// OpenGL widgets cannot have child widgets. Do not call this.
4537 	/* @disable */ final override void addChild(Widget, int) {
4538 		throw new Error("cannot add children to OpenGL widgets");
4539 	}
4540 
4541 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
4542 	/// Keep in mind that events like mouse coordinates are still relative to your size.
4543 	override void registerMovement() {
4544 		// writefln("%d %d %d %d", x,y,width,height);
4545 		version(win32_widgets)
4546 			auto pos = getChildPositionRelativeToParentHwnd(this);
4547 		else
4548 			auto pos = getChildPositionRelativeToParentOrigin(this);
4549 		win.moveResize(pos[0], pos[1], width, height);
4550 
4551 		registerMovementAdditionalWork();
4552 		sendResizeEvent();
4553 	}
4554 
4555 	abstract void registerMovementAdditionalWork();
4556 }
4557 
4558 /++
4559 	Nests an opengl capable window inside this window as a widget.
4560 
4561 	You may also just want to create an additional [SimpleWindow] with
4562 	[OpenGlOptions.yes] yourself.
4563 
4564 	An OpenGL widget cannot have child widgets. It will throw if you try.
4565 +/
4566 static if(OpenGlEnabled)
4567 class OpenGlWidget : NestedChildWindowWidget {
4568 
4569 	override void registerMovementAdditionalWork() {
4570 		win.setAsCurrentOpenGlContext();
4571 	}
4572 
4573 	///
4574 	this(Widget parent) {
4575 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4576 		super(win, parent);
4577 	}
4578 
4579 	override void paint(WidgetPainter painter) {
4580 		win.setAsCurrentOpenGlContext();
4581 		glViewport(0, 0, this.width, this.height);
4582 		win.redrawOpenGlSceneNow();
4583 	}
4584 
4585 	void redrawOpenGlScene(void delegate() dg) {
4586 		win.redrawOpenGlScene = dg;
4587 	}
4588 }
4589 
4590 /++
4591 	This demo shows how to draw text in an opengl scene.
4592 +/
4593 unittest {
4594 	import arsd.minigui;
4595 	import arsd.ttf;
4596 
4597 	void main() {
4598 		auto window = new Window();
4599 
4600 		auto widget = new OpenGlWidget(window);
4601 
4602 		// old means non-shader code so compatible with glBegin etc.
4603 		// tbh I haven't implemented new one in font yet...
4604 		// anyway, declaring here, will construct soon.
4605 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
4606 
4607 		// this is a little bit awkward, calling some methods through
4608 		// the underlying SimpleWindow `win` method, and you can't do this
4609 		// on a nanovega widget due to conflicts so I should probably fix
4610 		// the api to be a bit easier. But here it will work.
4611 		//
4612 		// Alternatively, you could load the font on the first draw, inside
4613 		// the redrawOpenGlScene, and keep a flag so you don't do it every
4614 		// time. That'd be a bit easier since the lib sets up the context
4615 		// by then guaranteed.
4616 		//
4617 		// But still, I wanna show this.
4618 		widget.win.visibleForTheFirstTime = delegate {
4619 			// must set the opengl context
4620 			widget.win.setAsCurrentOpenGlContext();
4621 
4622 			// if you were doing a OpenGL 3+ shader, this
4623 			// gets especially important to do in order. With
4624 			// old-style opengl, I think you can even do it
4625 			// in main(), but meh, let's show it more correctly.
4626 
4627 			// Anyway, now it is time to load the font from the
4628 			// OS (you can alternatively load one from a .ttf file
4629 			// you bundle with the application), then load the
4630 			// font into texture for drawing.
4631 
4632 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
4633 
4634 			assert(!osfont.isNull()); // make sure it actually loaded
4635 
4636 			// using typeof to avoid repeating the long name lol
4637 			glfont = new typeof(glfont)(
4638 				// get the raw data from the font for loading in here
4639 				// since it doesn't use the OS function to draw the
4640 				// text, we gotta treat it more as a file than as
4641 				// a drawing api.
4642 				osfont.getTtfBytes(),
4643 				18, // need to respecify size since opengl world is different coordinate system
4644 
4645 				// these last two numbers are why it is called
4646 				// "Limited" font. It only loads the characters
4647 				// in the given range, since the texture atlas
4648 				// it references is all a big image generated ahead
4649 				// of time. You could maybe do the whole thing but
4650 				// idk how much memory that is.
4651 				//
4652 				// But here, 0-128 represents the ASCII range, so
4653 				// good enough for most English things, numeric labels,
4654 				// etc.
4655 				0,
4656 				128
4657 			);
4658 		};
4659 
4660 		widget.redrawOpenGlScene = () {
4661 			// now we can use the glfont's drawString function
4662 
4663 			// first some opengl setup. You can do this in one place
4664 			// on window first visible too in many cases, just showing
4665 			// here cuz it is easier for me.
4666 
4667 			// gonna need some alpha blending or it just looks awful
4668 			glEnable(GL_BLEND);
4669 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
4670 			glClearColor(0,0,0,0);
4671 			glDepthFunc(GL_LEQUAL);
4672 
4673 			// Also need to enable 2d textures, since it draws the
4674 			// font characters as images baked in
4675 			glMatrixMode(GL_MODELVIEW);
4676 			glLoadIdentity();
4677 			glDisable(GL_DEPTH_TEST);
4678 			glEnable(GL_TEXTURE_2D);
4679 
4680 			// the orthographic matrix is best for 2d things like text
4681 			// so let's set that up. This matrix makes the coordinates
4682 			// in the opengl scene be one-to-one with the actual pixels
4683 			// on screen. (Not necessarily best, you may wish to scale
4684 			// things, but it does help keep fonts looking normal.)
4685 			glMatrixMode(GL_PROJECTION);
4686 			glLoadIdentity();
4687 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
4688 
4689 			// you can do other glScale, glRotate, glTranslate, etc
4690 			// to the matrix here of course if you want.
4691 
4692 			// note the x,y coordinates here are for the text baseline
4693 			// NOT the upper-left corner. The baseline is like the line
4694 			// in the notebook you write on. Most the letters are actually
4695 			// above it, but some, like p and q, dip a bit below it.
4696 			//
4697 			// So if you're used to the upper left coordinate like the
4698 			// rest of simpledisplay/minigui usually do, do the
4699 			// y + glfont.ascent to bring it down a little. So this
4700 			// example puts the string in the upper left of the window.
4701 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
4702 
4703 			// re color btw: the function sets a solid color internally,
4704 			// but you actually COULD do your own thing for rainbow effects
4705 			// and the sort if you wanted too, by pulling its guts out.
4706 			// Just view its source for an idea of how it actually draws:
4707 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
4708 
4709 			// it gets a bit complicated with the character positioning,
4710 			// but the opengl parts are fairly simple: bind a texture,
4711 			// set the color, draw a quad for each letter.
4712 
4713 
4714 			// the last optional argument there btw is a bounding box
4715 			// it will/ use to word wrap and return an object you can
4716 			// use to implement scrolling or pagination; it tells how
4717 			// much of the string didn't fit in the box. But for simple
4718 			// labels we can just ignore that.
4719 
4720 
4721 			// I'd suggest drawing text as the last step, after you
4722 			// do your other drawing. You might use the push/pop matrix
4723 			// stuff to keep your place. You, in theory, should be able
4724 			// to do text in a 3d space but I've never actually tried
4725 			// that....
4726 		};
4727 
4728 		window.loop();
4729 	}
4730 }
4731 
4732 version(custom_widgets)
4733 	private alias ListWidgetBase = ScrollableWidget;
4734 else
4735 	private alias ListWidgetBase = Widget;
4736 
4737 /++
4738 	A list widget contains a list of strings that the user can examine and select.
4739 
4740 
4741 	In the future, items in the list may be possible to be more than just strings.
4742 
4743 	See_Also:
4744 		[TableView]
4745 +/
4746 class ListWidget : ListWidgetBase {
4747 	/// 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.
4748 	mixin Emits!(ChangeEvent!void);
4749 
4750 	static struct Option {
4751 		string label;
4752 		bool selected;
4753 		void* tag;
4754 	}
4755 
4756 	/++
4757 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
4758 	+/
4759 	void setSelection(int y) {
4760 		if(!multiSelect)
4761 			foreach(ref opt; options)
4762 				opt.selected = false;
4763 		if(y >= 0 && y < options.length)
4764 			options[y].selected = !options[y].selected;
4765 
4766 		this.emit!(ChangeEvent!void)(delegate {});
4767 
4768 		version(custom_widgets)
4769 			redraw();
4770 	}
4771 
4772 	/++
4773 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
4774 		Returns -1 if nothing is selected.
4775 	+/
4776 	int getSelection()
4777 	{
4778 		foreach(i, opt; options) {
4779 			if (opt.selected)
4780 				return cast(int) i;
4781 		}
4782 		return -1;
4783 	}
4784 
4785 	version(custom_widgets)
4786 	override void defaultEventHandler_click(ClickEvent event) {
4787 		this.focus();
4788 		if(event.button == MouseButton.left) {
4789 			auto y = (event.clientY - 4) / defaultLineHeight;
4790 			if(y >= 0 && y < options.length) {
4791 				setSelection(y);
4792 			}
4793 		}
4794 		super.defaultEventHandler_click(event);
4795 	}
4796 
4797 	this(Widget parent) {
4798 		tabStop = false;
4799 		super(parent);
4800 		version(win32_widgets)
4801 			createWin32Window(this, WC_LISTBOX, "",
4802 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
4803 	}
4804 
4805 	version(win32_widgets)
4806 	override void handleWmCommand(ushort code, ushort id) {
4807 		switch(code) {
4808 			case LBN_SELCHANGE:
4809 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
4810 				setSelection(cast(int) sel);
4811 			break;
4812 			default:
4813 		}
4814 	}
4815 
4816 
4817 	version(custom_widgets)
4818 	override void paintFrameAndBackground(WidgetPainter painter) {
4819 		draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor);
4820 	}
4821 
4822 	version(custom_widgets)
4823 	override void paint(WidgetPainter painter) {
4824 		auto cs = getComputedStyle();
4825 		auto pos = Point(4, 4);
4826 		foreach(idx, option; options) {
4827 			painter.fillColor = painter.visualTheme.widgetBackgroundColor;
4828 			painter.outlineColor = painter.visualTheme.widgetBackgroundColor;
4829 			painter.drawRectangle(pos, width - 8, defaultLineHeight);
4830 			if(option.selected) {
4831 				//painter.rasterOp = RasterOp.xor;
4832 				painter.outlineColor = cs.selectionForegroundColor;
4833 				painter.fillColor = cs.selectionBackgroundColor;
4834 				painter.drawRectangle(pos, width - 8, defaultLineHeight);
4835 				//painter.rasterOp = RasterOp.normal;
4836 			}
4837 			painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor;
4838 			painter.drawText(pos, option.label);
4839 			pos.y += defaultLineHeight;
4840 		}
4841 	}
4842 
4843 	static class Style : Widget.Style {
4844 		override WidgetBackground background() {
4845 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
4846 		}
4847 	}
4848 	mixin OverrideStyle!Style;
4849 	//mixin Padding!q{2};
4850 
4851 	void addOption(string text, void* tag = null) {
4852 		options ~= Option(text, false, tag);
4853 		version(win32_widgets) {
4854 			WCharzBuffer buffer = WCharzBuffer(text);
4855 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
4856 		}
4857 		version(custom_widgets) {
4858 			setContentSize(width, cast(int) (options.length * defaultLineHeight));
4859 			redraw();
4860 		}
4861 	}
4862 
4863 	void clear() {
4864 		options = null;
4865 		version(win32_widgets) {
4866 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
4867 				{}
4868 
4869 		} else version(custom_widgets) {
4870 			scrollTo(Point(0, 0));
4871 			redraw();
4872 		}
4873 	}
4874 
4875 	Option[] options;
4876 	version(win32_widgets)
4877 		enum multiSelect = false; /// not implemented yet
4878 	else
4879 		bool multiSelect;
4880 
4881 	override int heightStretchiness() { return 6; }
4882 }
4883 
4884 
4885 
4886 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
4887 enum ScrollBarShowPolicy {
4888 	automatic, /// automatically show the scroll bar if it is necessary
4889 	never, /// never show the scroll bar (scrolling must be done programmatically)
4890 	always /// always show the scroll bar, even if it is disabled
4891 }
4892 
4893 /++
4894 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
4895 
4896 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
4897 +/
4898 // FIXME ScrollBarShowPolicy
4899 // FIXME: use the ScrollMessageWidget in here now that it exists
4900 class ScrollableWidget : Widget {
4901 	// FIXME: make line size configurable
4902 	// FIXME: add keyboard controls
4903 	version(win32_widgets) {
4904 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
4905 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
4906 				auto pos = HIWORD(wParam);
4907 				auto m = LOWORD(wParam);
4908 
4909 				// FIXME: I can reintroduce the
4910 				// scroll bars now by using this
4911 				// in the top-level window handler
4912 				// to forward comamnds
4913 				auto scrollbarHwnd = lParam;
4914 				switch(m) {
4915 					case SB_BOTTOM:
4916 						if(msg == WM_HSCROLL)
4917 							horizontalScrollTo(contentWidth_);
4918 						else
4919 							verticalScrollTo(contentHeight_);
4920 					break;
4921 					case SB_TOP:
4922 						if(msg == WM_HSCROLL)
4923 							horizontalScrollTo(0);
4924 						else
4925 							verticalScrollTo(0);
4926 					break;
4927 					case SB_ENDSCROLL:
4928 						// idk
4929 					break;
4930 					case SB_LINEDOWN:
4931 						if(msg == WM_HSCROLL)
4932 							horizontalScroll(scaleWithDpi(16));
4933 						else
4934 							verticalScroll(scaleWithDpi(16));
4935 					break;
4936 					case SB_LINEUP:
4937 						if(msg == WM_HSCROLL)
4938 							horizontalScroll(scaleWithDpi(-16));
4939 						else
4940 							verticalScroll(scaleWithDpi(-16));
4941 					break;
4942 					case SB_PAGEDOWN:
4943 						if(msg == WM_HSCROLL)
4944 							horizontalScroll(scaleWithDpi(100));
4945 						else
4946 							verticalScroll(scaleWithDpi(100));
4947 					break;
4948 					case SB_PAGEUP:
4949 						if(msg == WM_HSCROLL)
4950 							horizontalScroll(scaleWithDpi(-100));
4951 						else
4952 							verticalScroll(scaleWithDpi(-100));
4953 					break;
4954 					case SB_THUMBPOSITION:
4955 					case SB_THUMBTRACK:
4956 						if(msg == WM_HSCROLL)
4957 							horizontalScrollTo(pos);
4958 						else
4959 							verticalScrollTo(pos);
4960 
4961 						if(m == SB_THUMBTRACK) {
4962 							// the event loop doesn't seem to carry on with a requested redraw..
4963 							// so we request it to get our dirty bit set...
4964 							redraw();
4965 
4966 							// then we need to immediately actually redraw it too for instant feedback to user
4967 
4968 							SimpleWindow.processAllCustomEvents();
4969 							//if(parentWindow)
4970 								//parentWindow.actualRedraw();
4971 						}
4972 					break;
4973 					default:
4974 				}
4975 			}
4976 			return super.hookedWndProc(msg, wParam, lParam);
4977 		}
4978 	}
4979 	///
4980 	this(Widget parent) {
4981 		this.parentWindow = parent.parentWindow;
4982 
4983 		version(win32_widgets) {
4984 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
4985 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
4986 			super(parent);
4987 		} else version(custom_widgets) {
4988 			outerContainer = new InternalScrollableContainerWidget(this, parent);
4989 			super(outerContainer);
4990 		} else static assert(0);
4991 	}
4992 
4993 	version(custom_widgets)
4994 		InternalScrollableContainerWidget outerContainer;
4995 
4996 	override void defaultEventHandler_click(ClickEvent event) {
4997 		if(event.button == MouseButton.wheelUp)
4998 			verticalScroll(scaleWithDpi(-16));
4999 		if(event.button == MouseButton.wheelDown)
5000 			verticalScroll(scaleWithDpi(16));
5001 		super.defaultEventHandler_click(event);
5002 	}
5003 
5004 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5005 		switch(event.key) {
5006 			case Key.Left:
5007 				horizontalScroll(scaleWithDpi(-16));
5008 			break;
5009 			case Key.Right:
5010 				horizontalScroll(scaleWithDpi(16));
5011 			break;
5012 			case Key.Up:
5013 				verticalScroll(scaleWithDpi(-16));
5014 			break;
5015 			case Key.Down:
5016 				verticalScroll(scaleWithDpi(16));
5017 			break;
5018 			case Key.Home:
5019 				verticalScrollTo(0);
5020 			break;
5021 			case Key.End:
5022 				verticalScrollTo(contentHeight);
5023 			break;
5024 			case Key.PageUp:
5025 				verticalScroll(scaleWithDpi(-160));
5026 			break;
5027 			case Key.PageDown:
5028 				verticalScroll(scaleWithDpi(160));
5029 			break;
5030 			default:
5031 		}
5032 		super.defaultEventHandler_keydown(event);
5033 	}
5034 
5035 
5036 	version(win32_widgets)
5037 	override void recomputeChildLayout() {
5038 		super.recomputeChildLayout();
5039 		SCROLLINFO info;
5040 		info.cbSize = info.sizeof;
5041 		info.nPage = viewportHeight;
5042 		info.fMask = SIF_PAGE | SIF_RANGE;
5043 		info.nMin = 0;
5044 		info.nMax = contentHeight_;
5045 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5046 
5047 		info.cbSize = info.sizeof;
5048 		info.nPage = viewportWidth;
5049 		info.fMask = SIF_PAGE | SIF_RANGE;
5050 		info.nMin = 0;
5051 		info.nMax = contentWidth_;
5052 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5053 	}
5054 
5055 	/*
5056 		Scrolling
5057 		------------
5058 
5059 		You are assigned a width and a height by the layout engine, which
5060 		is your viewport box. However, you may draw more than that by setting
5061 		a contentWidth and contentHeight.
5062 
5063 		If these can be contained by the viewport, no scrollbar is displayed.
5064 		If they cannot fit though, it will automatically show scroll as necessary.
5065 
5066 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5067 		is zero, no vertical scrolling is performed.
5068 
5069 		If scrolling is necessary, the lib will automatically work with the bars.
5070 		When you redraw, the origin and clipping info in the painter is set so if
5071 		you just draw everything, it will work, but you can be more efficient by checking
5072 		the viewportWidth, viewportHeight, and scrollOrigin members.
5073 	*/
5074 
5075 	///
5076 	final @property int viewportWidth() {
5077 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5078 	}
5079 	///
5080 	final @property int viewportHeight() {
5081 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5082 	}
5083 
5084 	// FIXME property
5085 	Point scrollOrigin_;
5086 
5087 	///
5088 	final const(Point) scrollOrigin() {
5089 		return scrollOrigin_;
5090 	}
5091 
5092 	// the user sets these two
5093 	private int contentWidth_ = 0;
5094 	private int contentHeight_ = 0;
5095 
5096 	///
5097 	int contentWidth() { return contentWidth_; }
5098 	///
5099 	int contentHeight() { return contentHeight_; }
5100 
5101 	///
5102 	void setContentSize(int width, int height) {
5103 		contentWidth_ = width;
5104 		contentHeight_ = height;
5105 
5106 		version(custom_widgets) {
5107 			if(showingVerticalScroll || showingHorizontalScroll) {
5108 				outerContainer.queueRecomputeChildLayout();
5109 			}
5110 
5111 			if(showingVerticalScroll())
5112 				outerContainer.verticalScrollBar.redraw();
5113 			if(showingHorizontalScroll())
5114 				outerContainer.horizontalScrollBar.redraw();
5115 		} else version(win32_widgets) {
5116 			queueRecomputeChildLayout();
5117 		} else static assert(0);
5118 	}
5119 
5120 	///
5121 	void verticalScroll(int delta) {
5122 		verticalScrollTo(scrollOrigin.y + delta);
5123 	}
5124 	///
5125 	void verticalScrollTo(int pos) {
5126 		scrollOrigin_.y = pos;
5127 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5128 			scrollOrigin_.y = contentHeight - viewportHeight;
5129 
5130 		if(scrollOrigin_.y < 0)
5131 			scrollOrigin_.y = 0;
5132 
5133 		version(win32_widgets) {
5134 			SCROLLINFO info;
5135 			info.cbSize = info.sizeof;
5136 			info.fMask = SIF_POS;
5137 			info.nPos = scrollOrigin_.y;
5138 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5139 		} else version(custom_widgets) {
5140 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5141 		} else static assert(0);
5142 
5143 		redraw();
5144 	}
5145 
5146 	///
5147 	void horizontalScroll(int delta) {
5148 		horizontalScrollTo(scrollOrigin.x + delta);
5149 	}
5150 	///
5151 	void horizontalScrollTo(int pos) {
5152 		scrollOrigin_.x = pos;
5153 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5154 			scrollOrigin_.x = contentWidth - viewportWidth;
5155 
5156 		if(scrollOrigin_.x < 0)
5157 			scrollOrigin_.x = 0;
5158 
5159 		version(win32_widgets) {
5160 			SCROLLINFO info;
5161 			info.cbSize = info.sizeof;
5162 			info.fMask = SIF_POS;
5163 			info.nPos = scrollOrigin_.x;
5164 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5165 		} else version(custom_widgets) {
5166 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5167 		} else static assert(0);
5168 
5169 		redraw();
5170 	}
5171 	///
5172 	void scrollTo(Point p) {
5173 		verticalScrollTo(p.y);
5174 		horizontalScrollTo(p.x);
5175 	}
5176 
5177 	///
5178 	void ensureVisibleInScroll(Point p) {
5179 		auto rect = viewportRectangle();
5180 		if(rect.contains(p))
5181 			return;
5182 		if(p.x < rect.left)
5183 			horizontalScroll(p.x - rect.left);
5184 		else if(p.x > rect.right)
5185 			horizontalScroll(p.x - rect.right);
5186 
5187 		if(p.y < rect.top)
5188 			verticalScroll(p.y - rect.top);
5189 		else if(p.y > rect.bottom)
5190 			verticalScroll(p.y - rect.bottom);
5191 	}
5192 
5193 	///
5194 	void ensureVisibleInScroll(Rectangle rect) {
5195 		ensureVisibleInScroll(rect.upperLeft);
5196 		ensureVisibleInScroll(rect.lowerRight);
5197 	}
5198 
5199 	///
5200 	Rectangle viewportRectangle() {
5201 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5202 	}
5203 
5204 	///
5205 	bool showingHorizontalScroll() {
5206 		return contentWidth > width;
5207 	}
5208 	///
5209 	bool showingVerticalScroll() {
5210 		return contentHeight > height;
5211 	}
5212 
5213 	/// This is called before the ordinary paint delegate,
5214 	/// giving you a chance to draw the window frame, etc,
5215 	/// before the scroll clip takes effect
5216 	void paintFrameAndBackground(WidgetPainter painter) {
5217 		version(win32_widgets) {
5218 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5219 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5220 			// since the pen is null, to fill the whole space, we need the +1 on both.
5221 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5222 			SelectObject(painter.impl.hdc, p);
5223 			SelectObject(painter.impl.hdc, b);
5224 		}
5225 
5226 	}
5227 
5228 	// make space for the scroll bar, and that's it.
5229 	final override int paddingRight() { return scaleWithDpi(16); }
5230 	final override int paddingBottom() { return scaleWithDpi(16); }
5231 
5232 	/*
5233 		END SCROLLING
5234 	*/
5235 
5236 	override WidgetPainter draw() {
5237 		int x = this.x, y = this.y;
5238 		auto parent = this.parent;
5239 		while(parent) {
5240 			x += parent.x;
5241 			y += parent.y;
5242 			parent = parent.parent;
5243 		}
5244 
5245 		//version(win32_widgets) {
5246 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5247 		//} else {
5248 			auto painter = parentWindow.win.draw(true);
5249 		//}
5250 		painter.originX = x;
5251 		painter.originY = y;
5252 
5253 		painter.originX = painter.originX - scrollOrigin.x;
5254 		painter.originY = painter.originY - scrollOrigin.y;
5255 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
5256 
5257 		return WidgetPainter(painter, this);
5258 	}
5259 
5260 	mixin ScrollableChildren;
5261 }
5262 
5263 // you need to have a Point scrollOrigin in the class somewhere
5264 // and a paintFrameAndBackground
5265 private mixin template ScrollableChildren() {
5266 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5267 		if(hidden)
5268 			return;
5269 
5270 		//version(win32_widgets)
5271 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5272 
5273 		painter.originX = lox + x;
5274 		painter.originY = loy + y;
5275 
5276 		bool actuallyPainted = false;
5277 
5278 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
5279 		if(clip == Rectangle.init)
5280 			return;
5281 
5282 		if(force || redrawRequested) {
5283 			//painter.setClipRectangle(scrollOrigin, width, height);
5284 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5285 			paintFrameAndBackground(painter);
5286 		}
5287 
5288 		/+
5289 		version(win32_widgets) {
5290 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
5291 		}
5292 		+/
5293 
5294 		painter.originX = painter.originX - scrollOrigin.x;
5295 		painter.originY = painter.originY - scrollOrigin.y;
5296 		if(force || redrawRequested) {
5297 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
5298 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
5299 
5300 			//erase(painter); // we paintFrameAndBackground above so no need
5301 			if(painter.visualTheme)
5302 				painter.visualTheme.doPaint(this, painter);
5303 			else
5304 				paint(painter);
5305 
5306 			if(invalidate) {
5307 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5308 				// children are contained inside this, so no need to do extra work
5309 				invalidate = false;
5310 			}
5311 
5312 
5313 			actuallyPainted = true;
5314 			redrawRequested = false;
5315 		}
5316 
5317 		foreach(child; children) {
5318 			if(cast(FixedPosition) child)
5319 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5320 			else
5321 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5322 		}
5323 	}
5324 }
5325 
5326 private class InternalScrollableContainerInsideWidget : ContainerWidget {
5327 	ScrollableContainerWidget scw;
5328 
5329 	this(ScrollableContainerWidget parent) {
5330 		scw = parent;
5331 		super(parent);
5332 	}
5333 
5334 	version(custom_widgets)
5335 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5336 		if(hidden)
5337 			return;
5338 
5339 		bool actuallyPainted = false;
5340 
5341 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
5342 
5343 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
5344 		if(clip == Rectangle.init)
5345 			return;
5346 
5347 		painter.originX = lox + x - scrollOrigin.x;
5348 		painter.originY = loy + y - scrollOrigin.y;
5349 		if(force || redrawRequested) {
5350 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5351 
5352 			erase(painter);
5353 			if(painter.visualTheme)
5354 				painter.visualTheme.doPaint(this, painter);
5355 			else
5356 				paint(painter);
5357 
5358 			if(invalidate) {
5359 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5360 				// children are contained inside this, so no need to do extra work
5361 				invalidate = false;
5362 			}
5363 
5364 			actuallyPainted = true;
5365 			redrawRequested = false;
5366 		}
5367 		foreach(child; children) {
5368 			if(cast(FixedPosition) child)
5369 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5370 			else
5371 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5372 		}
5373 	}
5374 
5375 	version(custom_widgets)
5376 	override protected void addScrollPosition(ref int x, ref int y) {
5377 		x += scw.scrollX_;
5378 		y += scw.scrollY_;
5379 	}
5380 }
5381 
5382 /++
5383 	A widget meant to contain other widgets that may need to scroll.
5384 
5385 	Currently buggy.
5386 
5387 	History:
5388 		Added July 1, 2021 (dub v10.2)
5389 
5390 		On January 3, 2022, I tried to use it in a few other cases
5391 		and found it only worked well in the original test case. Since
5392 		it still sucks, I think I'm going to rewrite it again.
5393 +/
5394 class ScrollableContainerWidget : ContainerWidget {
5395 	///
5396 	this(Widget parent) {
5397 		super(parent);
5398 
5399 		container = new InternalScrollableContainerInsideWidget(this);
5400 		hsb = new HorizontalScrollbar(this);
5401 		vsb = new VerticalScrollbar(this);
5402 
5403 		tabStop = false;
5404 		container.tabStop = false;
5405 		magic = true;
5406 
5407 
5408 		vsb.addEventListener("scrolltonextline", () {
5409 			scrollBy(0, scaleWithDpi(16));
5410 		});
5411 		vsb.addEventListener("scrolltopreviousline", () {
5412 			scrollBy(0,scaleWithDpi( -16));
5413 		});
5414 		vsb.addEventListener("scrolltonextpage", () {
5415 			scrollBy(0, container.height);
5416 		});
5417 		vsb.addEventListener("scrolltopreviouspage", () {
5418 			scrollBy(0, -container.height);
5419 		});
5420 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
5421 			scrollTo(scrollX_, spe.value);
5422 		});
5423 
5424 		this.addEventListener(delegate (scope ClickEvent e) {
5425 			if(e.button == MouseButton.wheelUp) {
5426 				if(!e.defaultPrevented)
5427 					scrollBy(0, scaleWithDpi(-16));
5428 				e.stopPropagation();
5429 			} else if(e.button == MouseButton.wheelDown) {
5430 				if(!e.defaultPrevented)
5431 					scrollBy(0, scaleWithDpi(16));
5432 				e.stopPropagation();
5433 			}
5434 		});
5435 	}
5436 
5437 	/+
5438 	override void defaultEventHandler_click(ClickEvent e) {
5439 	}
5440 	+/
5441 
5442 	override void removeAllChildren() {
5443 		container.removeAllChildren();
5444 	}
5445 
5446 	void scrollTo(int x, int y) {
5447 		scrollBy(x - scrollX_, y - scrollY_);
5448 	}
5449 
5450 	void scrollBy(int x, int y) {
5451 		auto ox = scrollX_;
5452 		auto oy = scrollY_;
5453 
5454 		auto nx = ox + x;
5455 		auto ny = oy + y;
5456 
5457 		if(nx < 0)
5458 			nx = 0;
5459 		if(ny < 0)
5460 			ny = 0;
5461 
5462 		auto maxX = hsb.max - container.width;
5463 		if(maxX < 0) maxX = 0;
5464 		auto maxY = vsb.max - container.height;
5465 		if(maxY < 0) maxY = 0;
5466 
5467 		if(nx > maxX)
5468 			nx = maxX;
5469 		if(ny > maxY)
5470 			ny = maxY;
5471 
5472 		auto dx = nx - ox;
5473 		auto dy = ny - oy;
5474 
5475 		if(dx || dy) {
5476 			version(win32_widgets)
5477 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
5478 			else {
5479 				redraw();
5480 			}
5481 
5482 			hsb.setPosition = nx;
5483 			vsb.setPosition = ny;
5484 
5485 			scrollX_ = nx;
5486 			scrollY_ = ny;
5487 		}
5488 	}
5489 
5490 	private int scrollX_;
5491 	private int scrollY_;
5492 
5493 	void setTotalArea(int width, int height) {
5494 		hsb.setMax(width);
5495 		vsb.setMax(height);
5496 	}
5497 
5498 	///
5499 	void setViewableArea(int width, int height) {
5500 		hsb.setViewableArea(width);
5501 		vsb.setViewableArea(height);
5502 	}
5503 
5504 	private bool magic;
5505 	override void addChild(Widget w, int position = int.max) {
5506 		if(magic)
5507 			container.addChild(w, position);
5508 		else
5509 			super.addChild(w, position);
5510 	}
5511 
5512 	override void recomputeChildLayout() {
5513 		if(hsb is null || vsb is null || container is null) return;
5514 
5515 		/+
5516 		writeln(x, " ", y , " ", width, " ", height);
5517 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
5518 		+/
5519 
5520 		registerMovement();
5521 
5522 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
5523 		hsb.x = 0;
5524 		hsb.y = this.height - hsb.height;
5525 		hsb.width = this.width - scaleWithDpi(16);
5526 		hsb.recomputeChildLayout();
5527 
5528 		vsb.width = scaleWithDpi(16); // FIXME?
5529 		vsb.x = this.width - vsb.width;
5530 		vsb.y = 0;
5531 		vsb.height = this.height - scaleWithDpi(16);
5532 		vsb.recomputeChildLayout();
5533 
5534 		container.x = 0;
5535 		container.y = 0;
5536 		container.width = this.width - vsb.width;
5537 		container.height = this.height - hsb.height;
5538 		container.recomputeChildLayout();
5539 
5540 		scrollX_ = 0;
5541 		scrollY_ = 0;
5542 
5543 		hsb.setPosition(0);
5544 		vsb.setPosition(0);
5545 
5546 		int mw, mh;
5547 		Widget c = container;
5548 		// FIXME: hack here to handle a layout inside...
5549 		if(c.children.length == 1 && cast(Layout) c.children[0])
5550 			c = c.children[0];
5551 		foreach(child; c.children) {
5552 			auto w = child.x + child.width;
5553 			auto h = child.y + child.height;
5554 
5555 			if(w > mw) mw = w;
5556 			if(h > mh) mh = h;
5557 		}
5558 
5559 		setTotalArea(mw, mh);
5560 		setViewableArea(width, height);
5561 	}
5562 
5563 	override int minHeight() { return scaleWithDpi(64); }
5564 
5565 	HorizontalScrollbar hsb;
5566 	VerticalScrollbar vsb;
5567 	ContainerWidget container;
5568 }
5569 
5570 
5571 version(custom_widgets)
5572 private class InternalScrollableContainerWidget : Widget {
5573 
5574 	ScrollableWidget sw;
5575 
5576 	VerticalScrollbar verticalScrollBar;
5577 	HorizontalScrollbar horizontalScrollBar;
5578 
5579 	this(ScrollableWidget sw, Widget parent) {
5580 		this.sw = sw;
5581 
5582 		this.tabStop = false;
5583 
5584 		super(parent);
5585 
5586 		horizontalScrollBar = new HorizontalScrollbar(this);
5587 		verticalScrollBar = new VerticalScrollbar(this);
5588 
5589 		horizontalScrollBar.showing_ = false;
5590 		verticalScrollBar.showing_ = false;
5591 
5592 		horizontalScrollBar.addEventListener("scrolltonextline", {
5593 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
5594 			sw.horizontalScrollTo(horizontalScrollBar.position);
5595 		});
5596 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
5597 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
5598 			sw.horizontalScrollTo(horizontalScrollBar.position);
5599 		});
5600 		verticalScrollBar.addEventListener("scrolltonextline", {
5601 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
5602 			sw.verticalScrollTo(verticalScrollBar.position);
5603 		});
5604 		verticalScrollBar.addEventListener("scrolltopreviousline", {
5605 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
5606 			sw.verticalScrollTo(verticalScrollBar.position);
5607 		});
5608 		horizontalScrollBar.addEventListener("scrolltonextpage", {
5609 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
5610 			sw.horizontalScrollTo(horizontalScrollBar.position);
5611 		});
5612 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
5613 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
5614 			sw.horizontalScrollTo(horizontalScrollBar.position);
5615 		});
5616 		verticalScrollBar.addEventListener("scrolltonextpage", {
5617 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
5618 			sw.verticalScrollTo(verticalScrollBar.position);
5619 		});
5620 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
5621 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
5622 			sw.verticalScrollTo(verticalScrollBar.position);
5623 		});
5624 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
5625 			horizontalScrollBar.setPosition(event.intValue);
5626 			sw.horizontalScrollTo(horizontalScrollBar.position);
5627 		});
5628 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
5629 			verticalScrollBar.setPosition(event.intValue);
5630 			sw.verticalScrollTo(verticalScrollBar.position);
5631 		});
5632 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
5633 			horizontalScrollBar.setPosition(event.intValue);
5634 			sw.horizontalScrollTo(horizontalScrollBar.position);
5635 		});
5636 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
5637 			verticalScrollBar.setPosition(event.intValue);
5638 		});
5639 	}
5640 
5641 	// this is supposed to be basically invisible...
5642 	override int minWidth() { return sw.minWidth; }
5643 	override int minHeight() { return sw.minHeight; }
5644 	override int maxWidth() { return sw.maxWidth; }
5645 	override int maxHeight() { return sw.maxHeight; }
5646 	override int widthStretchiness() { return sw.widthStretchiness; }
5647 	override int heightStretchiness() { return sw.heightStretchiness; }
5648 	override int marginLeft() { return sw.marginLeft; }
5649 	override int marginRight() { return sw.marginRight; }
5650 	override int marginTop() { return sw.marginTop; }
5651 	override int marginBottom() { return sw.marginBottom; }
5652 	override int paddingLeft() { return sw.paddingLeft; }
5653 	override int paddingRight() { return sw.paddingRight; }
5654 	override int paddingTop() { return sw.paddingTop; }
5655 	override int paddingBottom() { return sw.paddingBottom; }
5656 	override void focus() { sw.focus(); }
5657 
5658 
5659 	override void recomputeChildLayout() {
5660 		// The stupid thing needs to calculate if a scroll bar is needed...
5661 		recomputeChildLayoutHelper();
5662 		// then running it again will position things correctly if the bar is NOT needed
5663 		recomputeChildLayoutHelper();
5664 
5665 		// this sucks but meh it barely works
5666 	}
5667 
5668 	private void recomputeChildLayoutHelper() {
5669 		if(sw is null) return;
5670 
5671 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
5672 		if(horizontalScrollBar && verticalScrollBar) {
5673 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
5674 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
5675 			horizontalScrollBar.x = 0;
5676 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
5677 
5678 			verticalScrollBar.width = verticalScrollBar.minWidth();
5679 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
5680 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
5681 			verticalScrollBar.y = 0 + 2;
5682 
5683 			sw.x = 0;
5684 			sw.y = 0;
5685 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
5686 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
5687 
5688 			if(sw.contentWidth_ <= this.width)
5689 				sw.scrollOrigin_.x = 0;
5690 			if(sw.contentHeight_ <= this.height)
5691 				sw.scrollOrigin_.y = 0;
5692 
5693 			horizontalScrollBar.recomputeChildLayout();
5694 			verticalScrollBar.recomputeChildLayout();
5695 			sw.recomputeChildLayout();
5696 		}
5697 
5698 		if(sw.contentWidth_ <= this.width)
5699 			sw.scrollOrigin_.x = 0;
5700 		if(sw.contentHeight_ <= this.height)
5701 			sw.scrollOrigin_.y = 0;
5702 
5703 		if(sw.showingHorizontalScroll())
5704 			horizontalScrollBar.showing(true, false);
5705 		else
5706 			horizontalScrollBar.showing(false, false);
5707 		if(sw.showingVerticalScroll())
5708 			verticalScrollBar.showing(true, false);
5709 		else
5710 			verticalScrollBar.showing(false, false);
5711 
5712 		verticalScrollBar.setViewableArea(sw.viewportHeight());
5713 		verticalScrollBar.setMax(sw.contentHeight);
5714 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
5715 
5716 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
5717 		horizontalScrollBar.setMax(sw.contentWidth);
5718 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
5719 	}
5720 }
5721 
5722 /*
5723 class ScrollableClientWidget : Widget {
5724 	this(Widget parent) {
5725 		super(parent);
5726 	}
5727 	override void paint(WidgetPainter p) {
5728 		parent.paint(p);
5729 	}
5730 }
5731 */
5732 
5733 /++
5734 	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.
5735 +/
5736 abstract class Slider : Widget {
5737 	this(int min, int max, int step, Widget parent) {
5738 		min_ = min;
5739 		max_ = max;
5740 		step_ = step;
5741 		page_ = step;
5742 		super(parent);
5743 	}
5744 
5745 	private int min_;
5746 	private int max_;
5747 	private int step_;
5748 	private int position_;
5749 	private int page_;
5750 
5751 	// selection start and selection end
5752 	// tics
5753 	// tooltip?
5754 	// some way to see and just type the value
5755 	// win32 buddy controls are labels
5756 
5757 	///
5758 	void setMin(int a) {
5759 		min_ = a;
5760 		version(custom_widgets)
5761 			redraw();
5762 		version(win32_widgets)
5763 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
5764 	}
5765 	///
5766 	int min() {
5767 		return min_;
5768 	}
5769 	///
5770 	void setMax(int a) {
5771 		max_ = a;
5772 		version(custom_widgets)
5773 			redraw();
5774 		version(win32_widgets)
5775 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
5776 	}
5777 	///
5778 	int max() {
5779 		return max_;
5780 	}
5781 	///
5782 	void setPosition(int a) {
5783 		if(a > max)
5784 			a = max;
5785 		if(a < min)
5786 			a = min;
5787 		position_ = a;
5788 		version(custom_widgets)
5789 			setPositionCustom(a);
5790 
5791 		version(win32_widgets)
5792 			setPositionWindows(a);
5793 	}
5794 	version(win32_widgets) {
5795 		protected abstract void setPositionWindows(int a);
5796 	}
5797 
5798 	protected abstract int win32direction();
5799 
5800 	/++
5801 		Alias for [position] for better compatibility with generic code.
5802 
5803 		History:
5804 			Added October 5, 2021
5805 	+/
5806 	@property int value() {
5807 		return position;
5808 	}
5809 
5810 	///
5811 	int position() {
5812 		return position_;
5813 	}
5814 	///
5815 	void setStep(int a) {
5816 		step_ = a;
5817 		version(win32_widgets)
5818 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
5819 	}
5820 	///
5821 	int step() {
5822 		return step_;
5823 	}
5824 	///
5825 	void setPageSize(int a) {
5826 		page_ = a;
5827 		version(win32_widgets)
5828 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
5829 	}
5830 	///
5831 	int pageSize() {
5832 		return page_;
5833 	}
5834 
5835 	private void notify() {
5836 		auto event = new ChangeEvent!int(this, &this.position);
5837 		event.dispatch();
5838 	}
5839 
5840 	version(win32_widgets)
5841 	void win32Setup(int style) {
5842 		createWin32Window(this, TRACKBAR_CLASS, "",
5843 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
5844 
5845 		// the trackbar sends the same messages as scroll, which
5846 		// our other layer sends as these... just gonna translate
5847 		// here
5848 		this.addDirectEventListener("scrolltoposition", (Event event) {
5849 			event.stopPropagation();
5850 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
5851 			notify();
5852 		});
5853 		this.addDirectEventListener("scrolltonextline", (Event event) {
5854 			event.stopPropagation();
5855 			this.setPosition(this.position + this.step_ * this.win32direction);
5856 			notify();
5857 		});
5858 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
5859 			event.stopPropagation();
5860 			this.setPosition(this.position - this.step_ * this.win32direction);
5861 			notify();
5862 		});
5863 		this.addDirectEventListener("scrolltonextpage", (Event event) {
5864 			event.stopPropagation();
5865 			this.setPosition(this.position + this.page_ * this.win32direction);
5866 			notify();
5867 		});
5868 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
5869 			event.stopPropagation();
5870 			this.setPosition(this.position - this.page_ * this.win32direction);
5871 			notify();
5872 		});
5873 
5874 		setMin(min_);
5875 		setMax(max_);
5876 		setStep(step_);
5877 		setPageSize(page_);
5878 	}
5879 
5880 	version(custom_widgets) {
5881 		protected MouseTrackingWidget thumb;
5882 
5883 		protected abstract void setPositionCustom(int a);
5884 
5885 		override void defaultEventHandler_keydown(KeyDownEvent event) {
5886 			switch(event.key) {
5887 				case Key.Up:
5888 				case Key.Right:
5889 					setPosition(position() - step() * win32direction);
5890 					changed();
5891 				break;
5892 				case Key.Down:
5893 				case Key.Left:
5894 					setPosition(position() + step() * win32direction);
5895 					changed();
5896 				break;
5897 				case Key.Home:
5898 					setPosition(win32direction > 0 ? min() : max());
5899 					changed();
5900 				break;
5901 				case Key.End:
5902 					setPosition(win32direction > 0 ? max() : min());
5903 					changed();
5904 				break;
5905 				case Key.PageUp:
5906 					setPosition(position() - pageSize() * win32direction);
5907 					changed();
5908 				break;
5909 				case Key.PageDown:
5910 					setPosition(position() + pageSize() * win32direction);
5911 					changed();
5912 				break;
5913 				default:
5914 			}
5915 			super.defaultEventHandler_keydown(event);
5916 		}
5917 
5918 		protected void changed() {
5919 			auto ev = new ChangeEvent!int(this, &position);
5920 			ev.dispatch();
5921 		}
5922 	}
5923 }
5924 
5925 /++
5926 
5927 +/
5928 class VerticalSlider : Slider {
5929 	this(int min, int max, int step, Widget parent) {
5930 		version(custom_widgets)
5931 			initialize();
5932 
5933 		super(min, max, step, parent);
5934 
5935 		version(win32_widgets)
5936 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
5937 	}
5938 
5939 	protected override int win32direction() {
5940 		return -1;
5941 	}
5942 
5943 	version(win32_widgets)
5944 	protected override void setPositionWindows(int a) {
5945 		// the windows thing makes the top 0 and i don't like that.
5946 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
5947 	}
5948 
5949 	version(custom_widgets)
5950 	private void initialize() {
5951 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
5952 
5953 		thumb.tabStop = false;
5954 
5955 		thumb.thumbWidth = width;
5956 		thumb.thumbHeight = scaleWithDpi(16);
5957 
5958 		thumb.addEventListener(EventType.change, () {
5959 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
5960 			sx = max - sx;
5961 			//informProgramThatUserChangedPosition(sx);
5962 
5963 			position_ = sx;
5964 
5965 			changed();
5966 		});
5967 	}
5968 
5969 	version(custom_widgets)
5970 	override void recomputeChildLayout() {
5971 		thumb.thumbWidth = this.width;
5972 		super.recomputeChildLayout();
5973 		setPositionCustom(position_);
5974 	}
5975 
5976 	version(custom_widgets)
5977 	protected override void setPositionCustom(int a) {
5978 		if(max())
5979 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
5980 		redraw();
5981 	}
5982 }
5983 
5984 /++
5985 
5986 +/
5987 class HorizontalSlider : Slider {
5988 	this(int min, int max, int step, Widget parent) {
5989 		version(custom_widgets)
5990 			initialize();
5991 
5992 		super(min, max, step, parent);
5993 
5994 		version(win32_widgets)
5995 			win32Setup(TBS_HORZ);
5996 	}
5997 
5998 	version(win32_widgets)
5999 	protected override void setPositionWindows(int a) {
6000 		SendMessage(hwnd, TBM_SETPOS, true, a);
6001 	}
6002 
6003 	protected override int win32direction() {
6004 		return 1;
6005 	}
6006 
6007 	version(custom_widgets)
6008 	private void initialize() {
6009 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6010 
6011 		thumb.tabStop = false;
6012 
6013 		thumb.thumbWidth = scaleWithDpi(16);
6014 		thumb.thumbHeight = height;
6015 
6016 		thumb.addEventListener(EventType.change, () {
6017 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6018 			//informProgramThatUserChangedPosition(sx);
6019 
6020 			position_ = sx;
6021 
6022 			changed();
6023 		});
6024 	}
6025 
6026 	version(custom_widgets)
6027 	override void recomputeChildLayout() {
6028 		thumb.thumbHeight = this.height;
6029 		super.recomputeChildLayout();
6030 		setPositionCustom(position_);
6031 	}
6032 
6033 	version(custom_widgets)
6034 	protected override void setPositionCustom(int a) {
6035 		if(max())
6036 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6037 		redraw();
6038 	}
6039 }
6040 
6041 
6042 ///
6043 abstract class ScrollbarBase : Widget {
6044 	///
6045 	this(Widget parent) {
6046 		super(parent);
6047 		tabStop = false;
6048 		step_ = scaleWithDpi(16);
6049 	}
6050 
6051 	private int viewableArea_;
6052 	private int max_;
6053 	private int step_;// = 16;
6054 	private int position_;
6055 
6056 	///
6057 	bool atEnd() {
6058 		return position_ + viewableArea_ >= max_;
6059 	}
6060 
6061 	///
6062 	bool atStart() {
6063 		return position_ == 0;
6064 	}
6065 
6066 	///
6067 	void setViewableArea(int a) {
6068 		viewableArea_ = a;
6069 		version(custom_widgets)
6070 			redraw();
6071 	}
6072 	///
6073 	void setMax(int a) {
6074 		max_ = a;
6075 		version(custom_widgets)
6076 			redraw();
6077 	}
6078 	///
6079 	int max() {
6080 		return max_;
6081 	}
6082 	///
6083 	void setPosition(int a) {
6084 		auto logicalMax = max_ - viewableArea_;
6085 		if(a == int.max)
6086 			a = logicalMax;
6087 
6088 		if(a > logicalMax)
6089 			a = logicalMax;
6090 		if(a < 0)
6091 			a = 0;
6092 
6093 		position_ = a;
6094 
6095 		version(custom_widgets)
6096 			redraw();
6097 	}
6098 	///
6099 	int position() {
6100 		return position_;
6101 	}
6102 	///
6103 	void setStep(int a) {
6104 		step_ = a;
6105 	}
6106 	///
6107 	int step() {
6108 		return step_;
6109 	}
6110 
6111 	// FIXME: remove this.... maybe
6112 	/+
6113 	protected void informProgramThatUserChangedPosition(int n) {
6114 		position_ = n;
6115 		auto evt = new Event(EventType.change, this);
6116 		evt.intValue = n;
6117 		evt.dispatch();
6118 	}
6119 	+/
6120 
6121 	version(custom_widgets) {
6122 		enum MIN_THUMB_SIZE = 8;
6123 
6124 		abstract protected int getBarDim();
6125 		int thumbSize() {
6126 			if(viewableArea_ >= max_ || max_ == 0)
6127 				return getBarDim();
6128 
6129 			int res = viewableArea_ * getBarDim() / max_;
6130 
6131 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6132 				res = scaleWithDpi(MIN_THUMB_SIZE);
6133 
6134 			return res;
6135 		}
6136 
6137 		int thumbPosition() {
6138 			/*
6139 				viewableArea_ is the viewport height/width
6140 				position_ is where we are
6141 			*/
6142 			//if(position_ + viewableArea_ >= max_)
6143 				//return getBarDim - thumbSize;
6144 
6145 			auto maximumPossibleValue = getBarDim() - thumbSize;
6146 			auto maximiumLogicalValue = max_ - viewableArea_;
6147 
6148 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6149 
6150 			return p;
6151 		}
6152 	}
6153 }
6154 
6155 //public import mgt;
6156 
6157 /++
6158 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6159 
6160 	Concrete subclasses may include a scrollbar thumb and a volume control.
6161 +/
6162 //version(custom_widgets)
6163 class MouseTrackingWidget : Widget {
6164 
6165 	///
6166 	int positionX() { return positionX_; }
6167 	///
6168 	int positionY() { return positionY_; }
6169 
6170 	///
6171 	void positionX(int p) { positionX_ = p; }
6172 	///
6173 	void positionY(int p) { positionY_ = p; }
6174 
6175 	private int positionX_;
6176 	private int positionY_;
6177 
6178 	///
6179 	enum Orientation {
6180 		horizontal, ///
6181 		vertical, ///
6182 		twoDimensional, ///
6183 	}
6184 
6185 	private int thumbWidth_;
6186 	private int thumbHeight_;
6187 
6188 	///
6189 	int thumbWidth() { return thumbWidth_; }
6190 	///
6191 	int thumbHeight() { return thumbHeight_; }
6192 	///
6193 	int thumbWidth(int a) { return thumbWidth_ = a; }
6194 	///
6195 	int thumbHeight(int a) { return thumbHeight_ = a; }
6196 
6197 	private bool dragging;
6198 	private bool hovering;
6199 	private int startMouseX, startMouseY;
6200 
6201 	///
6202 	this(Orientation orientation, Widget parent) {
6203 		super(parent);
6204 
6205 		//assert(parentWindow !is null);
6206 
6207 		addEventListener((MouseDownEvent event) {
6208 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6209 				dragging = true;
6210 				startMouseX = event.clientX - positionX;
6211 				startMouseY = event.clientY - positionY;
6212 				parentWindow.captureMouse(this);
6213 			} else {
6214 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6215 					positionX = event.clientX - thumbWidth / 2;
6216 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6217 					positionY = event.clientY - thumbHeight / 2;
6218 
6219 				if(positionX + thumbWidth > this.width)
6220 					positionX = this.width - thumbWidth;
6221 				if(positionY + thumbHeight > this.height)
6222 					positionY = this.height - thumbHeight;
6223 
6224 				if(positionX < 0)
6225 					positionX = 0;
6226 				if(positionY < 0)
6227 					positionY = 0;
6228 
6229 
6230 				// this.emit!(ChangeEvent!void)();
6231 				auto evt = new Event(EventType.change, this);
6232 				evt.sendDirectly();
6233 
6234 				redraw();
6235 
6236 			}
6237 		});
6238 
6239 		addEventListener(EventType.mouseup, (Event event) {
6240 			dragging = false;
6241 			parentWindow.releaseMouseCapture();
6242 		});
6243 
6244 		addEventListener(EventType.mouseout, (Event event) {
6245 			if(!hovering)
6246 				return;
6247 			hovering = false;
6248 			redraw();
6249 		});
6250 
6251 		int lpx, lpy;
6252 
6253 		addEventListener((MouseMoveEvent event) {
6254 			auto oh = hovering;
6255 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6256 				hovering = true;
6257 			} else {
6258 				hovering = false;
6259 			}
6260 			if(!dragging) {
6261 				if(hovering != oh)
6262 					redraw();
6263 				return;
6264 			}
6265 
6266 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6267 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
6268 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6269 				positionY = event.clientY - startMouseY;
6270 
6271 			if(positionX + thumbWidth > this.width)
6272 				positionX = this.width - thumbWidth;
6273 			if(positionY + thumbHeight > this.height)
6274 				positionY = this.height - thumbHeight;
6275 
6276 			if(positionX < 0)
6277 				positionX = 0;
6278 			if(positionY < 0)
6279 				positionY = 0;
6280 
6281 			if(positionX != lpx || positionY != lpy) {
6282 				lpx = positionX;
6283 				lpy = positionY;
6284 
6285 				auto evt = new Event(EventType.change, this);
6286 				evt.sendDirectly();
6287 			}
6288 
6289 			redraw();
6290 		});
6291 	}
6292 
6293 	version(custom_widgets)
6294 	override void paint(WidgetPainter painter) {
6295 		auto cs = getComputedStyle();
6296 		auto c = darken(cs.windowBackgroundColor, 0.2);
6297 		painter.outlineColor = c;
6298 		painter.fillColor = c;
6299 		painter.drawRectangle(Point(0, 0), this.width, this.height);
6300 
6301 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
6302 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
6303 	}
6304 }
6305 
6306 //version(custom_widgets)
6307 //private
6308 class HorizontalScrollbar : ScrollbarBase {
6309 
6310 	version(custom_widgets) {
6311 		private MouseTrackingWidget thumb;
6312 
6313 		override int getBarDim() {
6314 			return thumb.width;
6315 		}
6316 	}
6317 
6318 	override void setViewableArea(int a) {
6319 		super.setViewableArea(a);
6320 
6321 		version(win32_widgets) {
6322 			SCROLLINFO info;
6323 			info.cbSize = info.sizeof;
6324 			info.nPage = a + 1;
6325 			info.fMask = SIF_PAGE;
6326 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6327 		} else version(custom_widgets) {
6328 			thumb.positionX = thumbPosition;
6329 			thumb.thumbWidth = thumbSize;
6330 			thumb.redraw();
6331 		} else static assert(0);
6332 
6333 	}
6334 
6335 	override void setMax(int a) {
6336 		super.setMax(a);
6337 		version(win32_widgets) {
6338 			SCROLLINFO info;
6339 			info.cbSize = info.sizeof;
6340 			info.nMin = 0;
6341 			info.nMax = max;
6342 			info.fMask = SIF_RANGE;
6343 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6344 		} else version(custom_widgets) {
6345 			thumb.positionX = thumbPosition;
6346 			thumb.thumbWidth = thumbSize;
6347 			thumb.redraw();
6348 		}
6349 	}
6350 
6351 	override void setPosition(int a) {
6352 		super.setPosition(a);
6353 		version(win32_widgets) {
6354 			SCROLLINFO info;
6355 			info.cbSize = info.sizeof;
6356 			info.fMask = SIF_POS;
6357 			info.nPos = position;
6358 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6359 		} else version(custom_widgets) {
6360 			thumb.positionX = thumbPosition();
6361 			thumb.thumbWidth = thumbSize;
6362 			thumb.redraw();
6363 		} else static assert(0);
6364 	}
6365 
6366 	this(Widget parent) {
6367 		super(parent);
6368 
6369 		version(win32_widgets) {
6370 			createWin32Window(this, "Scrollbar"w, "",
6371 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
6372 		} else version(custom_widgets) {
6373 			auto vl = new HorizontalLayout(this);
6374 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
6375 			leftButton.setClickRepeat(scrollClickRepeatInterval);
6376 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
6377 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
6378 			rightButton.setClickRepeat(scrollClickRepeatInterval);
6379 
6380 			leftButton.tabStop = false;
6381 			rightButton.tabStop = false;
6382 			thumb.tabStop = false;
6383 
6384 			leftButton.addEventListener(EventType.triggered, () {
6385 				this.emitCommand!"scrolltopreviousline"();
6386 				//informProgramThatUserChangedPosition(position - step());
6387 			});
6388 			rightButton.addEventListener(EventType.triggered, () {
6389 				this.emitCommand!"scrolltonextline"();
6390 				//informProgramThatUserChangedPosition(position + step());
6391 			});
6392 
6393 			thumb.thumbWidth = this.minWidth;
6394 			thumb.thumbHeight = scaleWithDpi(16);
6395 
6396 			thumb.addEventListener(EventType.change, () {
6397 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
6398 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
6399 
6400 				//informProgramThatUserChangedPosition(sx);
6401 
6402 				auto ev = new ScrollToPositionEvent(this, sx);
6403 				ev.dispatch();
6404 			});
6405 		}
6406 	}
6407 
6408 	override int minHeight() { return scaleWithDpi(16); }
6409 	override int maxHeight() { return scaleWithDpi(16); }
6410 	override int minWidth() { return scaleWithDpi(48); }
6411 }
6412 
6413 class ScrollToPositionEvent : Event {
6414 	enum EventString = "scrolltoposition";
6415 
6416 	this(Widget target, int value) {
6417 		this.value = value;
6418 		super(EventString, target);
6419 	}
6420 
6421 	immutable int value;
6422 
6423 	override @property int intValue() {
6424 		return value;
6425 	}
6426 }
6427 
6428 //version(custom_widgets)
6429 //private
6430 class VerticalScrollbar : ScrollbarBase {
6431 
6432 	version(custom_widgets) {
6433 		override int getBarDim() {
6434 			return thumb.height;
6435 		}
6436 
6437 		private MouseTrackingWidget thumb;
6438 	}
6439 
6440 	override void setViewableArea(int a) {
6441 		super.setViewableArea(a);
6442 
6443 		version(win32_widgets) {
6444 			SCROLLINFO info;
6445 			info.cbSize = info.sizeof;
6446 			info.nPage = a + 1;
6447 			info.fMask = SIF_PAGE;
6448 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6449 		} else version(custom_widgets) {
6450 			thumb.positionY = thumbPosition;
6451 			thumb.thumbHeight = thumbSize;
6452 			thumb.redraw();
6453 		} else static assert(0);
6454 
6455 	}
6456 
6457 	override void setMax(int a) {
6458 		super.setMax(a);
6459 		version(win32_widgets) {
6460 			SCROLLINFO info;
6461 			info.cbSize = info.sizeof;
6462 			info.nMin = 0;
6463 			info.nMax = max;
6464 			info.fMask = SIF_RANGE;
6465 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6466 		} else version(custom_widgets) {
6467 			thumb.positionY = thumbPosition;
6468 			thumb.thumbHeight = thumbSize;
6469 			thumb.redraw();
6470 		}
6471 	}
6472 
6473 	override void setPosition(int a) {
6474 		super.setPosition(a);
6475 		version(win32_widgets) {
6476 			SCROLLINFO info;
6477 			info.cbSize = info.sizeof;
6478 			info.fMask = SIF_POS;
6479 			info.nPos = position;
6480 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6481 		} else version(custom_widgets) {
6482 			thumb.positionY = thumbPosition;
6483 			thumb.thumbHeight = thumbSize;
6484 			thumb.redraw();
6485 		} else static assert(0);
6486 	}
6487 
6488 	this(Widget parent) {
6489 		super(parent);
6490 
6491 		version(win32_widgets) {
6492 			createWin32Window(this, "Scrollbar"w, "",
6493 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
6494 		} else version(custom_widgets) {
6495 			auto vl = new VerticalLayout(this);
6496 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
6497 			upButton.setClickRepeat(scrollClickRepeatInterval);
6498 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
6499 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
6500 			downButton.setClickRepeat(scrollClickRepeatInterval);
6501 
6502 			upButton.addEventListener(EventType.triggered, () {
6503 				this.emitCommand!"scrolltopreviousline"();
6504 				//informProgramThatUserChangedPosition(position - step());
6505 			});
6506 			downButton.addEventListener(EventType.triggered, () {
6507 				this.emitCommand!"scrolltonextline"();
6508 				//informProgramThatUserChangedPosition(position + step());
6509 			});
6510 
6511 			thumb.thumbWidth = this.minWidth;
6512 			thumb.thumbHeight = scaleWithDpi(16);
6513 
6514 			thumb.addEventListener(EventType.change, () {
6515 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
6516 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
6517 
6518 				auto ev = new ScrollToPositionEvent(this, sy);
6519 				ev.dispatch();
6520 
6521 				//informProgramThatUserChangedPosition(sy);
6522 			});
6523 
6524 			upButton.tabStop = false;
6525 			downButton.tabStop = false;
6526 			thumb.tabStop = false;
6527 		}
6528 	}
6529 
6530 	override int minWidth() { return scaleWithDpi(16); }
6531 	override int maxWidth() { return scaleWithDpi(16); }
6532 	override int minHeight() { return scaleWithDpi(48); }
6533 }
6534 
6535 
6536 /++
6537 	EXPERIMENTAL
6538 
6539 	A widget specialized for being a container for other widgets.
6540 
6541 	History:
6542 		Added May 29, 2021. Not stabilized at this time.
6543 +/
6544 class WidgetContainer : Widget {
6545 	this(Widget parent) {
6546 		tabStop = false;
6547 		super(parent);
6548 	}
6549 
6550 	override int maxHeight() {
6551 		if(this.children.length == 1) {
6552 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
6553 		} else {
6554 			return int.max;
6555 		}
6556 	}
6557 
6558 	override int maxWidth() {
6559 		if(this.children.length == 1) {
6560 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
6561 		} else {
6562 			return int.max;
6563 		}
6564 	}
6565 
6566 	/+
6567 
6568 	override int minHeight() {
6569 		int largest = 0;
6570 		int margins = 0;
6571 		int lastMargin = 0;
6572 		foreach(child; children) {
6573 			auto mh = child.minHeight();
6574 			if(mh > largest)
6575 				largest = mh;
6576 			margins += mymax(lastMargin, child.marginTop());
6577 			lastMargin = child.marginBottom();
6578 		}
6579 		return largest + margins;
6580 	}
6581 
6582 	override int maxHeight() {
6583 		int largest = 0;
6584 		int margins = 0;
6585 		int lastMargin = 0;
6586 		foreach(child; children) {
6587 			auto mh = child.maxHeight();
6588 			if(mh == int.max)
6589 				return int.max;
6590 			if(mh > largest)
6591 				largest = mh;
6592 			margins += mymax(lastMargin, child.marginTop());
6593 			lastMargin = child.marginBottom();
6594 		}
6595 		return largest + margins;
6596 	}
6597 
6598 	override int minWidth() {
6599 		int min;
6600 		foreach(child; children) {
6601 			auto cm = child.minWidth;
6602 			if(cm > min)
6603 				min = cm;
6604 		}
6605 		return min + paddingLeft + paddingRight;
6606 	}
6607 
6608 	override int minHeight() {
6609 		int min;
6610 		foreach(child; children) {
6611 			auto cm = child.minHeight;
6612 			if(cm > min)
6613 				min = cm;
6614 		}
6615 		return min + paddingTop + paddingBottom;
6616 	}
6617 
6618 	override int maxHeight() {
6619 		int largest = 0;
6620 		int margins = 0;
6621 		int lastMargin = 0;
6622 		foreach(child; children) {
6623 			auto mh = child.maxHeight();
6624 			if(mh == int.max)
6625 				return int.max;
6626 			if(mh > largest)
6627 				largest = mh;
6628 			margins += mymax(lastMargin, child.marginTop());
6629 			lastMargin = child.marginBottom();
6630 		}
6631 		return largest + margins;
6632 	}
6633 
6634 	override int heightStretchiness() {
6635 		int max;
6636 		foreach(child; children) {
6637 			auto c = child.heightStretchiness;
6638 			if(c > max)
6639 				max = c;
6640 		}
6641 		return max;
6642 	}
6643 
6644 	override int marginTop() {
6645 		if(this.children.length)
6646 			return this.children[0].marginTop;
6647 		return 0;
6648 	}
6649 	+/
6650 }
6651 
6652 ///
6653 abstract class Layout : Widget {
6654 	this(Widget parent) {
6655 		tabStop = false;
6656 		super(parent);
6657 	}
6658 }
6659 
6660 /++
6661 	Makes all children minimum width and height, placing them down
6662 	left to right, top to bottom.
6663 
6664 	Useful if you want to make a list of buttons that automatically
6665 	wrap to a new line when necessary.
6666 +/
6667 class InlineBlockLayout : Layout {
6668 	///
6669 	this(Widget parent) { super(parent); }
6670 
6671 	override void recomputeChildLayout() {
6672 		registerMovement();
6673 
6674 		int x = this.paddingLeft, y = this.paddingTop;
6675 
6676 		int lineHeight;
6677 		int previousMargin = 0;
6678 		int previousMarginBottom = 0;
6679 
6680 		foreach(child; children) {
6681 			if(child.hidden)
6682 				continue;
6683 			if(cast(FixedPosition) child) {
6684 				child.recomputeChildLayout();
6685 				continue;
6686 			}
6687 			child.width = child.flexBasisWidth();
6688 			if(child.width == 0)
6689 				child.width = child.minWidth();
6690 			if(child.width == 0)
6691 				child.width = 32;
6692 
6693 			child.height = child.flexBasisHeight();
6694 			if(child.height == 0)
6695 				child.height = child.minHeight();
6696 			if(child.height == 0)
6697 				child.height = 32;
6698 
6699 			if(x + child.width + paddingRight > this.width) {
6700 				x = this.paddingLeft;
6701 				y += lineHeight;
6702 				lineHeight = 0;
6703 				previousMargin = 0;
6704 				previousMarginBottom = 0;
6705 			}
6706 
6707 			auto margin = child.marginLeft;
6708 			if(previousMargin > margin)
6709 				margin = previousMargin;
6710 
6711 			x += margin;
6712 
6713 			child.x = x;
6714 			child.y = y;
6715 
6716 			int marginTopApplied;
6717 			if(child.marginTop > previousMarginBottom) {
6718 				child.y += child.marginTop;
6719 				marginTopApplied = child.marginTop;
6720 			}
6721 
6722 			x += child.width;
6723 			previousMargin = child.marginRight;
6724 
6725 			if(child.marginBottom > previousMarginBottom)
6726 				previousMarginBottom = child.marginBottom;
6727 
6728 			auto h = child.height + previousMarginBottom + marginTopApplied;
6729 			if(h > lineHeight)
6730 				lineHeight = h;
6731 
6732 			child.recomputeChildLayout();
6733 		}
6734 
6735 	}
6736 
6737 	override int minWidth() {
6738 		int min;
6739 		foreach(child; children) {
6740 			auto cm = child.minWidth;
6741 			if(cm > min)
6742 				min = cm;
6743 		}
6744 		return min + paddingLeft + paddingRight;
6745 	}
6746 
6747 	override int minHeight() {
6748 		int min;
6749 		foreach(child; children) {
6750 			auto cm = child.minHeight;
6751 			if(cm > min)
6752 				min = cm;
6753 		}
6754 		return min + paddingTop + paddingBottom;
6755 	}
6756 }
6757 
6758 /++
6759 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
6760 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
6761 	the [TabWidget] will automatically change pages of child widgets.
6762 
6763 	This allows you to react to it however you see fit rather than having to
6764 	be tied to just the new sets of child widgets.
6765 
6766 	It sends the message in the form of `this.emitCommand!"changetab"();`.
6767 
6768 	History:
6769 		Added December 24, 2021 (dub v10.5)
6770 +/
6771 class TabMessageWidget : Widget {
6772 
6773 	protected void tabIndexClicked(int item) {
6774 		this.emitCommand!"changetab"();
6775 	}
6776 
6777 	/++
6778 		Adds the a new tab to the control with the given title.
6779 
6780 		Returns:
6781 			The index of the newly added tab. You will need to know
6782 			this index to refer to it later and to know which tab to
6783 			change to when you get a changetab message.
6784 	+/
6785 	int addTab(string title, int pos = int.max) {
6786 		version(win32_widgets) {
6787 			TCITEM item;
6788 			item.mask = TCIF_TEXT;
6789 			WCharzBuffer buf = WCharzBuffer(title);
6790 			item.pszText = buf.ptr;
6791 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
6792 		} else version(custom_widgets) {
6793 			if(pos >= tabs.length) {
6794 				tabs ~= title;
6795 				redraw();
6796 				return cast(int) tabs.length - 1;
6797 			} else if(pos <= 0) {
6798 				tabs = title ~ tabs;
6799 				redraw();
6800 				return 0;
6801 			} else {
6802 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
6803 				redraw();
6804 				return pos;
6805 			}
6806 		}
6807 	}
6808 
6809 	override void addChild(Widget child, int pos = int.max) {
6810 		if(container)
6811 			container.addChild(child, pos);
6812 		else
6813 			super.addChild(child, pos);
6814 	}
6815 
6816 	protected Widget makeContainer() {
6817 		return new Widget(this);
6818 	}
6819 
6820 	private Widget container;
6821 
6822 	override void recomputeChildLayout() {
6823 		version(win32_widgets) {
6824 			this.registerMovement();
6825 
6826 			RECT rect;
6827 			GetWindowRect(hwnd, &rect);
6828 
6829 			auto left = rect.left;
6830 			auto top = rect.top;
6831 
6832 			TabCtrl_AdjustRect(hwnd, false, &rect);
6833 			foreach(child; children) {
6834 				if(!child.showing) continue;
6835 				child.x = rect.left - left;
6836 				child.y = rect.top - top;
6837 				child.width = rect.right - rect.left;
6838 				child.height = rect.bottom - rect.top;
6839 				child.recomputeChildLayout();
6840 			}
6841 		} else version(custom_widgets) {
6842 			this.registerMovement();
6843 			foreach(child; children) {
6844 				if(!child.showing) continue;
6845 				child.x = 2;
6846 				child.y = tabBarHeight + 2; // for the border
6847 				child.width = width - 4; // for the border
6848 				child.height = height - tabBarHeight - 2 - 2; // for the border
6849 				child.recomputeChildLayout();
6850 			}
6851 		} else static assert(0);
6852 	}
6853 
6854 	version(custom_widgets)
6855 		string[] tabs;
6856 
6857 	this(Widget parent) {
6858 		super(parent);
6859 
6860 		tabStop = false;
6861 
6862 		version(win32_widgets) {
6863 			createWin32Window(this, WC_TABCONTROL, "", 0);
6864 		} else version(custom_widgets) {
6865 			addEventListener((ClickEvent event) {
6866 				if(event.target !is this)
6867 					return;
6868 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
6869 					auto t = (event.clientX / tabWidth);
6870 					if(t >= 0 && t < tabs.length) {
6871 						currentTab_ = t;
6872 						tabIndexClicked(t);
6873 						redraw();
6874 					}
6875 				}
6876 			});
6877 		} else static assert(0);
6878 
6879 		this.container = makeContainer();
6880 	}
6881 
6882 	override int marginTop() { return 4; }
6883 	override int paddingBottom() { return 4; }
6884 
6885 	override int minHeight() {
6886 		int max = 0;
6887 		foreach(child; children)
6888 			max = mymax(child.minHeight, max);
6889 
6890 
6891 		version(win32_widgets) {
6892 			RECT rect;
6893 			rect.right = this.width;
6894 			rect.bottom = max;
6895 			TabCtrl_AdjustRect(hwnd, true, &rect);
6896 
6897 			max = rect.bottom;
6898 		} else {
6899 			max += defaultLineHeight + 4;
6900 		}
6901 
6902 
6903 		return max;
6904 	}
6905 
6906 	version(win32_widgets)
6907 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
6908 		switch(code) {
6909 			case TCN_SELCHANGE:
6910 				auto sel = TabCtrl_GetCurSel(hwnd);
6911 				tabIndexClicked(sel);
6912 			break;
6913 			default:
6914 		}
6915 		return 0;
6916 	}
6917 
6918 	version(custom_widgets) {
6919 		private int currentTab_;
6920 		private int tabBarHeight() { return defaultLineHeight; }
6921 		int tabWidth() { return scaleWithDpi(80); }
6922 	}
6923 
6924 	version(win32_widgets)
6925 	override void paint(WidgetPainter painter) {}
6926 
6927 	version(custom_widgets)
6928 	override void paint(WidgetPainter painter) {
6929 		auto cs = getComputedStyle();
6930 
6931 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
6932 
6933 		int posX = 0;
6934 		foreach(idx, title; tabs) {
6935 			auto isCurrent = idx == getCurrentTab();
6936 
6937 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
6938 
6939 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
6940 			painter.outlineColor = cs.foregroundColor;
6941 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
6942 
6943 			if(isCurrent) {
6944 				painter.outlineColor = cs.windowBackgroundColor;
6945 				painter.fillColor = Color.transparent;
6946 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
6947 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
6948 
6949 				painter.outlineColor = Color.white;
6950 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
6951 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
6952 				painter.outlineColor = cs.activeTabColor;
6953 				painter.drawPixel(Point(posX, tabBarHeight - 1));
6954 			}
6955 
6956 			posX += tabWidth - 2;
6957 		}
6958 	}
6959 
6960 	///
6961 	@scriptable
6962 	void setCurrentTab(int item) {
6963 		version(win32_widgets)
6964 			TabCtrl_SetCurSel(hwnd, item);
6965 		else version(custom_widgets)
6966 			currentTab_ = item;
6967 		else static assert(0);
6968 
6969 		tabIndexClicked(item);
6970 	}
6971 
6972 	///
6973 	@scriptable
6974 	int getCurrentTab() {
6975 		version(win32_widgets)
6976 			return TabCtrl_GetCurSel(hwnd);
6977 		else version(custom_widgets)
6978 			return currentTab_; // FIXME
6979 		else static assert(0);
6980 	}
6981 
6982 	///
6983 	@scriptable
6984 	void removeTab(int item) {
6985 		if(item && item == getCurrentTab())
6986 			setCurrentTab(item - 1);
6987 
6988 		version(win32_widgets) {
6989 			TabCtrl_DeleteItem(hwnd, item);
6990 		}
6991 
6992 		for(int a = item; a < children.length - 1; a++)
6993 			this._children[a] = this._children[a + 1];
6994 		this._children = this._children[0 .. $-1];
6995 	}
6996 
6997 }
6998 
6999 
7000 /++
7001 	A tab widget is a set of clickable tab buttons followed by a content area.
7002 
7003 
7004 	Tabs can change existing content or can be new pages.
7005 
7006 	When the user picks a different tab, a `change` message is generated.
7007 +/
7008 class TabWidget : TabMessageWidget {
7009 	this(Widget parent) {
7010 		super(parent);
7011 	}
7012 
7013 	override protected Widget makeContainer() {
7014 		return null;
7015 	}
7016 
7017 	override void addChild(Widget child, int pos = int.max) {
7018 		if(auto twp = cast(TabWidgetPage) child) {
7019 			Widget.addChild(child, pos);
7020 			if(pos == int.max)
7021 				pos = cast(int) this.children.length - 1;
7022 
7023 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7024 
7025 			if(pos != getCurrentTab) {
7026 				child.showing = false;
7027 			}
7028 		} else {
7029 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7030 		}
7031 	}
7032 
7033 	// FIXME: add tab icons at some point, Windows supports them
7034 	/++
7035 		Adds a page and its associated tab with the given label to the widget.
7036 
7037 		Returns:
7038 			The added page object, to which you can add other widgets.
7039 	+/
7040 	@scriptable
7041 	TabWidgetPage addPage(string title) {
7042 		return new TabWidgetPage(title, this);
7043 	}
7044 
7045 	/++
7046 		Gets the page at the given tab index, or `null` if the index is bad.
7047 
7048 		History:
7049 			Added December 24, 2021.
7050 	+/
7051 	TabWidgetPage getPage(int index) {
7052 		if(index < this.children.length)
7053 			return null;
7054 		return cast(TabWidgetPage) this.children[index];
7055 	}
7056 
7057 	/++
7058 		While you can still use the addTab from the parent class,
7059 		*strongly* recommend you use [addPage] insteaad.
7060 
7061 		History:
7062 			Added December 24, 2021 to fulful the interface
7063 			requirement that came from adding [TabMessageWidget].
7064 
7065 			You should not use it though since the [addPage] function
7066 			is much easier to use here.
7067 	+/
7068 	override int addTab(string title, int pos = int.max) {
7069 		auto p = addPage(title);
7070 		foreach(idx, child; this.children)
7071 			if(child is p)
7072 				return cast(int) idx;
7073 		return -1;
7074 	}
7075 
7076 	protected override void tabIndexClicked(int item) {
7077 		foreach(idx, child; children) {
7078 			child.showing(false, false); // batch the recalculates for the end
7079 		}
7080 
7081 		foreach(idx, child; children) {
7082 			if(idx == item) {
7083 				child.showing(true, false);
7084 				if(parentWindow) {
7085 					auto f = parentWindow.getFirstFocusable(child);
7086 					if(f)
7087 						f.focus();
7088 				}
7089 				recomputeChildLayout();
7090 			}
7091 		}
7092 
7093 		version(win32_widgets) {
7094 			InvalidateRect(hwnd, null, true);
7095 		} else version(custom_widgets) {
7096 			this.redraw();
7097 		}
7098 	}
7099 
7100 }
7101 
7102 /++
7103 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7104 
7105 	You add [TabWidgetPage]s to it.
7106 +/
7107 class PageWidget : Widget {
7108 	this(Widget parent) {
7109 		super(parent);
7110 	}
7111 
7112 	override int minHeight() {
7113 		int max = 0;
7114 		foreach(child; children)
7115 			max = mymax(child.minHeight, max);
7116 
7117 		return max;
7118 	}
7119 
7120 
7121 	override void addChild(Widget child, int pos = int.max) {
7122 		if(auto twp = cast(TabWidgetPage) child) {
7123 			super.addChild(child, pos);
7124 			if(pos == int.max)
7125 				pos = cast(int) this.children.length - 1;
7126 
7127 			if(pos != getCurrentTab) {
7128 				child.showing = false;
7129 			}
7130 		} else {
7131 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7132 		}
7133 	}
7134 
7135 	override void recomputeChildLayout() {
7136 		this.registerMovement();
7137 		foreach(child; children) {
7138 			child.x = 0;
7139 			child.y = 0;
7140 			child.width = width;
7141 			child.height = height;
7142 			child.recomputeChildLayout();
7143 		}
7144 	}
7145 
7146 	private int currentTab_;
7147 
7148 	///
7149 	@scriptable
7150 	void setCurrentTab(int item) {
7151 		currentTab_ = item;
7152 
7153 		showOnly(item);
7154 	}
7155 
7156 	///
7157 	@scriptable
7158 	int getCurrentTab() {
7159 		return currentTab_;
7160 	}
7161 
7162 	///
7163 	@scriptable
7164 	void removeTab(int item) {
7165 		if(item && item == getCurrentTab())
7166 			setCurrentTab(item - 1);
7167 
7168 		for(int a = item; a < children.length - 1; a++)
7169 			this._children[a] = this._children[a + 1];
7170 		this._children = this._children[0 .. $-1];
7171 	}
7172 
7173 	///
7174 	@scriptable
7175 	TabWidgetPage addPage(string title) {
7176 		return new TabWidgetPage(title, this);
7177 	}
7178 
7179 	private void showOnly(int item) {
7180 		foreach(idx, child; children)
7181 			if(idx == item) {
7182 				child.show();
7183 				child.queueRecomputeChildLayout();
7184 			} else {
7185 				child.hide();
7186 			}
7187 	}
7188 
7189 }
7190 
7191 /++
7192 
7193 +/
7194 class TabWidgetPage : Widget {
7195 	string title;
7196 	this(string title, Widget parent) {
7197 		this.title = title;
7198 		this.tabStop = false;
7199 		super(parent);
7200 
7201 		///*
7202 		version(win32_widgets) {
7203 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7204 		}
7205 		//*/
7206 	}
7207 
7208 	override int minHeight() {
7209 		int sum = 0;
7210 		foreach(child; children)
7211 			sum += child.minHeight();
7212 		return sum;
7213 	}
7214 }
7215 
7216 version(none)
7217 /++
7218 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7219 
7220 	I think I need to modify the layout algorithms to support this.
7221 +/
7222 class CollapsableSidebar : Widget {
7223 
7224 }
7225 
7226 /// Stacks the widgets vertically, taking all the available width for each child.
7227 class VerticalLayout : Layout {
7228 	// most of this is intentionally blank - widget's default is vertical layout right now
7229 	///
7230 	this(Widget parent) { super(parent); }
7231 
7232 	/++
7233 		Sets a max width for the layout so you don't have to subclass. The max width
7234 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7235 
7236 		History:
7237 			Added November 29, 2021 (dub v10.5)
7238 	+/
7239 	this(int maxWidth, Widget parent) {
7240 		this.mw = maxWidth;
7241 		super(parent);
7242 	}
7243 
7244 	private int mw = int.max;
7245 
7246 	override int maxWidth() { return scaleWithDpi(mw); }
7247 }
7248 
7249 /// Stacks the widgets horizontally, taking all the available height for each child.
7250 class HorizontalLayout : Layout {
7251 	///
7252 	this(Widget parent) { super(parent); }
7253 
7254 	/++
7255 		Sets a max height for the layout so you don't have to subclass. The max height
7256 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7257 
7258 		History:
7259 			Added November 29, 2021 (dub v10.5)
7260 	+/
7261 	this(int maxHeight, Widget parent) {
7262 		this.mh = maxHeight;
7263 		super(parent);
7264 	}
7265 
7266 	private int mh = 0;
7267 
7268 
7269 
7270 	override void recomputeChildLayout() {
7271 		.recomputeChildLayout!"width"(this);
7272 	}
7273 
7274 	override int minHeight() {
7275 		int largest = 0;
7276 		int margins = 0;
7277 		int lastMargin = 0;
7278 		foreach(child; children) {
7279 			auto mh = child.minHeight();
7280 			if(mh > largest)
7281 				largest = mh;
7282 			margins += mymax(lastMargin, child.marginTop());
7283 			lastMargin = child.marginBottom();
7284 		}
7285 		return largest + margins;
7286 	}
7287 
7288 	override int maxHeight() {
7289 		if(mh != 0)
7290 			return mymax(minHeight, scaleWithDpi(mh));
7291 
7292 		int largest = 0;
7293 		int margins = 0;
7294 		int lastMargin = 0;
7295 		foreach(child; children) {
7296 			auto mh = child.maxHeight();
7297 			if(mh == int.max)
7298 				return int.max;
7299 			if(mh > largest)
7300 				largest = mh;
7301 			margins += mymax(lastMargin, child.marginTop());
7302 			lastMargin = child.marginBottom();
7303 		}
7304 		return largest + margins;
7305 	}
7306 
7307 	override int heightStretchiness() {
7308 		int max;
7309 		foreach(child; children) {
7310 			auto c = child.heightStretchiness;
7311 			if(c > max)
7312 				max = c;
7313 		}
7314 		return max;
7315 	}
7316 
7317 }
7318 
7319 version(win32_widgets)
7320 private
7321 extern(Windows)
7322 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
7323 	Widget* pwin = hwnd in Widget.nativeMapping;
7324 	if(pwin is null)
7325 		return DefWindowProc(hwnd, message, wparam, lparam);
7326 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
7327 	if(win is null)
7328 		return DefWindowProc(hwnd, message, wparam, lparam);
7329 
7330 	switch(message) {
7331 		case WM_SIZE:
7332 			auto width = LOWORD(lparam);
7333 			auto height = HIWORD(lparam);
7334 
7335 			auto hdc = GetDC(hwnd);
7336 			auto hdcBmp = CreateCompatibleDC(hdc);
7337 
7338 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
7339 			if(width > win.bmpWidth || height > win.bmpHeight) {
7340 				auto oldBuffer = win.buffer;
7341 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
7342 
7343 				if(oldBuffer)
7344 					DeleteObject(oldBuffer);
7345 
7346 				win.bmpWidth = width;
7347 				win.bmpHeight = height;
7348 			}
7349 
7350 			// just always erase it upon resizing so minigui can draw over with a clean slate
7351 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
7352 
7353 			auto brush = GetSysColorBrush(COLOR_3DFACE);
7354 			RECT r;
7355 			r.left = 0;
7356 			r.top = 0;
7357 			r.right = width;
7358 			r.bottom = height;
7359 			FillRect(hdcBmp, &r, brush);
7360 
7361 			SelectObject(hdcBmp, oldBmp);
7362 			DeleteDC(hdcBmp);
7363 			ReleaseDC(hwnd, hdc);
7364 		break;
7365 		case WM_PAINT:
7366 			if(win.buffer is null)
7367 				goto default;
7368 
7369 			BITMAP bm;
7370 			PAINTSTRUCT ps;
7371 
7372 			HDC hdc = BeginPaint(hwnd, &ps);
7373 
7374 			HDC hdcMem = CreateCompatibleDC(hdc);
7375 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
7376 
7377 			GetObject(win.buffer, bm.sizeof, &bm);
7378 
7379 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
7380 
7381 			SelectObject(hdcMem, hbmOld);
7382 			DeleteDC(hdcMem);
7383 			EndPaint(hwnd, &ps);
7384 		break;
7385 		default:
7386 			return DefWindowProc(hwnd, message, wparam, lparam);
7387 	}
7388 
7389 	return 0;
7390 }
7391 
7392 private wstring Win32Class(wstring name)() {
7393 	static bool classRegistered;
7394 	if(!classRegistered) {
7395 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
7396 		WNDCLASSEX wc;
7397 		wc.cbSize = wc.sizeof;
7398 		wc.hInstance = hInstance;
7399 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
7400 		wc.lpfnWndProc = &DoubleBufferWndProc;
7401 		wc.lpszClassName = name.ptr;
7402 		if(!RegisterClassExW(&wc))
7403 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
7404 		classRegistered = true;
7405 	}
7406 
7407 		return name;
7408 }
7409 
7410 /+
7411 version(win32_widgets)
7412 extern(Windows)
7413 private
7414 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
7415 	switch(iMessage) {
7416 		case WM_PAINT:
7417 			if(auto te = hWnd in Widget.nativeMapping) {
7418 				try {
7419 					//te.redraw();
7420 					writeln(te, " drawing");
7421 				} catch(Exception) {}
7422 			}
7423 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7424 		default:
7425 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7426 	}
7427 }
7428 +/
7429 
7430 
7431 /++
7432 	A widget specifically designed to hold other widgets.
7433 
7434 	History:
7435 		Added July 1, 2021
7436 +/
7437 class ContainerWidget : Widget {
7438 	this(Widget parent) {
7439 		super(parent);
7440 		this.tabStop = false;
7441 
7442 		version(win32_widgets) {
7443 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
7444 		}
7445 	}
7446 }
7447 
7448 /++
7449 	A widget that takes your widget, puts scroll bars around it, and sends
7450 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
7451 	no effort to automatically scroll or clip its child widgets - it just sends
7452 	the messages.
7453 
7454 
7455 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
7456 	The scroll coordinates are all given in a unit you interpret as you wish. One
7457 	of these units is moved on each press of the arrow buttons and represents the
7458 	smallest amount the user can scroll. The intention is for this to be one line,
7459 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
7460 	in each direction that the user might be interested in.
7461 
7462 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
7463 	This is the amount it jumps when the user pressed page up and page down, or clicks
7464 	in the exposed part of the scroll bar.
7465 
7466 	You should add child content to the ScrollMessageWidget. However, it is important to
7467 	note that the coordinates are always independent of the scroll position! It is YOUR
7468 	responsibility to do any necessary transforms, clipping, etc., while drawing the
7469 	content and interpreting mouse events if they are supposed to change with the scroll.
7470 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
7471 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
7472 	you more control (which can be considerably more efficient and adapted to your actual data)
7473 	at the expense of you also needing to be aware of its reality.
7474 
7475 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
7476 	version 10.3. Maybe this will change in the future.... but for now you must call
7477 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
7478 +/
7479 class ScrollMessageWidget : Widget {
7480 	this(Widget parent) {
7481 		super(parent);
7482 
7483 		container = new Widget(this);
7484 		hsb = new HorizontalScrollbar(this);
7485 		vsb = new VerticalScrollbar(this);
7486 
7487 		hsb.addEventListener("scrolltonextline", {
7488 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
7489 			notify();
7490 		});
7491 		hsb.addEventListener("scrolltopreviousline", {
7492 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
7493 			notify();
7494 		});
7495 		vsb.addEventListener("scrolltonextline", {
7496 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
7497 			notify();
7498 		});
7499 		vsb.addEventListener("scrolltopreviousline", {
7500 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
7501 			notify();
7502 		});
7503 		hsb.addEventListener("scrolltonextpage", {
7504 			hsb.setPosition(hsb.position + hsb.step_);
7505 			notify();
7506 		});
7507 		hsb.addEventListener("scrolltopreviouspage", {
7508 			hsb.setPosition(hsb.position - hsb.step_);
7509 			notify();
7510 		});
7511 		vsb.addEventListener("scrolltonextpage", {
7512 			vsb.setPosition(vsb.position + vsb.step_);
7513 			notify();
7514 		});
7515 		vsb.addEventListener("scrolltopreviouspage", {
7516 			vsb.setPosition(vsb.position - vsb.step_);
7517 			notify();
7518 		});
7519 		hsb.addEventListener("scrolltoposition", (Event event) {
7520 			hsb.setPosition(event.intValue);
7521 			notify();
7522 		});
7523 		vsb.addEventListener("scrolltoposition", (Event event) {
7524 			vsb.setPosition(event.intValue);
7525 			notify();
7526 		});
7527 
7528 
7529 		tabStop = false;
7530 		container.tabStop = false;
7531 		magic = true;
7532 	}
7533 
7534 	private int movementPerButtonClickH_ = 1;
7535 	private int movementPerButtonClickV_ = 1;
7536 	public void movementPerButtonClick(int h, int v) {
7537 		movementPerButtonClickH_ = h;
7538 		movementPerButtonClickV_ = v;
7539 	}
7540 
7541 	/++
7542 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
7543 
7544 
7545 		The defaults for [addDefaultWheelListeners] are:
7546 
7547 			$(LIST
7548 				* Mouse wheel scrolls vertically
7549 				* Alt key + mouse wheel scrolls horiontally
7550 				* Shift + mouse wheel scrolls faster.
7551 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
7552 			)
7553 
7554 		The defaults for [addDefaultKeyboardListeners] are:
7555 
7556 			$(LIST
7557 				* Arrow keys scroll by the given amounts
7558 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
7559 				* Page up and down scroll by the vertical viewable area
7560 				* Home and end scroll to the start and end of the verticle viewable area.
7561 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
7562 			)
7563 
7564 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
7565 
7566 		Params:
7567 			horizontalArrowScrollAmount =
7568 			verticalArrowScrollAmount =
7569 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
7570 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
7571 			shiftMultiplier = multiplies the scroll amount by this when shift is held
7572 	+/
7573 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
7574 		auto _this = this;
7575 
7576 		container.addEventListener((scope KeyDownEvent ke) {
7577 			switch(ke.key) {
7578 				case Key.Left:
7579 					_this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7580 				break;
7581 				case Key.Right:
7582 					_this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7583 				break;
7584 				case Key.Up:
7585 					_this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7586 				break;
7587 				case Key.Down:
7588 					_this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7589 				break;
7590 				case Key.PageUp:
7591 					if(ke.altKey)
7592 						_this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7593 					else
7594 						_this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7595 				break;
7596 				case Key.PageDown:
7597 					if(ke.altKey)
7598 						_this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7599 					else
7600 						_this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7601 				break;
7602 				case Key.Home:
7603 					if(ke.altKey)
7604 						_this.scrollLeft(short.max * 16);
7605 					else
7606 						_this.scrollUp(short.max * 16);
7607 				break;
7608 				case Key.End:
7609 					if(ke.altKey)
7610 						_this.scrollRight(short.max * 16);
7611 					else
7612 						_this.scrollDown(short.max * 16);
7613 				break;
7614 
7615 				default:
7616 					// ignore, not for us.
7617 			}
7618 
7619 		});
7620 	}
7621 
7622 	/// ditto
7623 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
7624 		auto _this = this;
7625 		container.addEventListener((scope ClickEvent ce) {
7626 
7627 			//if(ce.target && ce.target.tabStop)
7628 				//ce.target.focus();
7629 
7630 			// ctrl is reserved for the application
7631 			if(ce.ctrlKey)
7632 				return;
7633 
7634 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
7635 				return;
7636 
7637 			if(shiftMultiplier == 0 && ce.shiftKey)
7638 				return;
7639 
7640 			if(ce.button == MouseButton.wheelDown) {
7641 				if(ce.altKey)
7642 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7643 				else
7644 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7645 			} else if(ce.button == MouseButton.wheelUp) {
7646 				if(ce.altKey)
7647 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7648 				else
7649 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7650 			}
7651 		});
7652 	}
7653 
7654 	/++
7655 		Scrolls the given amount.
7656 
7657 		History:
7658 			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.
7659 	+/
7660 	void scrollUp(int amount = 1) {
7661 		vsb.setPosition(vsb.position - amount);
7662 		notify();
7663 	}
7664 	/// ditto
7665 	void scrollDown(int amount = 1) {
7666 		vsb.setPosition(vsb.position + amount);
7667 		notify();
7668 	}
7669 	/// ditto
7670 	void scrollLeft(int amount = 1) {
7671 		hsb.setPosition(hsb.position - amount);
7672 		notify();
7673 	}
7674 	/// ditto
7675 	void scrollRight(int amount = 1) {
7676 		hsb.setPosition(hsb.position + amount);
7677 		notify();
7678 	}
7679 
7680 	///
7681 	VerticalScrollbar verticalScrollBar() { return vsb; }
7682 	///
7683 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
7684 
7685 	void notify() {
7686 		static bool insideNotify;
7687 
7688 		if(insideNotify)
7689 			return; // avoid the recursive call, even if it isn't strictly correct
7690 
7691 		insideNotify = true;
7692 		scope(exit) insideNotify = false;
7693 
7694 		this.emit!ScrollEvent();
7695 	}
7696 
7697 	mixin Emits!ScrollEvent;
7698 
7699 	///
7700 	Point position() {
7701 		return Point(hsb.position, vsb.position);
7702 	}
7703 
7704 	///
7705 	void setPosition(int x, int y) {
7706 		hsb.setPosition(x);
7707 		vsb.setPosition(y);
7708 	}
7709 
7710 	///
7711 	void setPageSize(int unitsX, int unitsY) {
7712 		hsb.setStep(unitsX);
7713 		vsb.setStep(unitsY);
7714 	}
7715 
7716 	/// Always call this BEFORE setViewableArea
7717 	void setTotalArea(int width, int height) {
7718 		hsb.setMax(width);
7719 		vsb.setMax(height);
7720 	}
7721 
7722 	/++
7723 		Always set the viewable area AFTER setitng the total area if you are going to change both.
7724 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
7725 		If you need to do that, use [queueRecomputeChildLayout].
7726 	+/
7727 	void setViewableArea(int width, int height) {
7728 
7729 		// actually there IS A need to dothis cuz the max might have changed since then
7730 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
7731 			//return; // no need to do what is already done
7732 		hsb.setViewableArea(width);
7733 		vsb.setViewableArea(height);
7734 
7735 		bool needsNotify = false;
7736 
7737 		// FIXME: if at any point the rhs is outside the scrollbar, we need
7738 		// to reset to 0. but it should remember the old position in case the
7739 		// window resizes again, so it can kinda return ot where it was.
7740 		//
7741 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
7742 		if(width >= hsb.max) {
7743 			// there's plenty of room to display it all so we need to reset to zero
7744 			// FIXME: adjust so it matches the note above
7745 			hsb.setPosition(0);
7746 			needsNotify = true;
7747 		}
7748 		if(height >= vsb.max) {
7749 			// there's plenty of room to display it all so we need to reset to zero
7750 			// FIXME: adjust so it matches the note above
7751 			vsb.setPosition(0);
7752 			needsNotify = true;
7753 		}
7754 		if(needsNotify)
7755 			notify();
7756 	}
7757 
7758 	private bool magic;
7759 	override void addChild(Widget w, int position = int.max) {
7760 		if(magic)
7761 			container.addChild(w, position);
7762 		else
7763 			super.addChild(w, position);
7764 	}
7765 
7766 	override void recomputeChildLayout() {
7767 		if(hsb is null || vsb is null || container is null) return;
7768 
7769 		registerMovement();
7770 
7771 		enum BUTTON_SIZE = 16;
7772 
7773 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
7774 		hsb.x = 0;
7775 		hsb.y = this.height - hsb.height;
7776 
7777 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
7778 		vsb.x = this.width - vsb.width;
7779 		vsb.y = 0;
7780 
7781 		auto vsb_width = vsb.showing ? vsb.width : 0;
7782 		auto hsb_height = hsb.showing ? hsb.height : 0;
7783 
7784 		hsb.width = this.width - vsb_width;
7785 		vsb.height = this.height - hsb_height;
7786 
7787 		hsb.recomputeChildLayout();
7788 		vsb.recomputeChildLayout();
7789 
7790 		if(this.header is null) {
7791 			container.x = 0;
7792 			container.y = 0;
7793 			container.width = this.width - vsb_width;
7794 			container.height = this.height - hsb_height;
7795 			container.recomputeChildLayout();
7796 		} else {
7797 			header.x = 0;
7798 			header.y = 0;
7799 			header.width = this.width - vsb_width;
7800 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
7801 			header.recomputeChildLayout();
7802 
7803 			container.x = 0;
7804 			container.y = scaleWithDpi(BUTTON_SIZE);
7805 			container.width = this.width - vsb_width;
7806 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
7807 			container.recomputeChildLayout();
7808 		}
7809 	}
7810 
7811 	private HorizontalScrollbar hsb;
7812 	private VerticalScrollbar vsb;
7813 	Widget container;
7814 	private Widget header;
7815 
7816 	/++
7817 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
7818 
7819 		History:
7820 			Added September 27, 2021 (dub v10.3)
7821 	+/
7822 	Widget getHeader() {
7823 		if(this.header is null) {
7824 			magic = false;
7825 			scope(exit) magic = true;
7826 			this.header = new Widget(this);
7827 			queueRecomputeChildLayout();
7828 		}
7829 		return this.header;
7830 	}
7831 
7832 	/++
7833 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
7834 
7835 		History:
7836 			Added January 3, 2023 (dub v11.0)
7837 	+/
7838 	void scrollIntoView(Rectangle rect) {
7839 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
7840 
7841 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
7842 
7843 		// the lower right is exclusive normally
7844 		auto test = rect.lowerRight;
7845 		if(test.x > 0) test.x--;
7846 		if(test.y > 0) test.y--;
7847 
7848 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
7849 			// try to scroll only one dimension at a time if we can
7850 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
7851 				setPosition(rect.upperLeft.x, position.y);
7852 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
7853 				setPosition(position.x, rect.upperLeft.y);
7854 		}
7855 
7856 	}
7857 
7858 	override int minHeight() {
7859 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
7860 		if(header !is null)
7861 			min += header.minHeight;
7862 		if(horizontalScrollBar.showing)
7863 			min += horizontalScrollBar.minHeight;
7864 		return min;
7865 	}
7866 
7867 	override int maxHeight() {
7868 		int max = container ? container.maxHeight : int.max;
7869 		if(max == int.max)
7870 			return max;
7871 		if(horizontalScrollBar.showing)
7872 			max += horizontalScrollBar.minHeight;
7873 		return max;
7874 	}
7875 }
7876 
7877 /++
7878 	$(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")
7879 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
7880 +/
7881 version(minigui_screenshots)
7882 @Screenshot("ScrollMessageWidget")
7883 unittest {
7884 	auto window = new Window("ScrollMessageWidget");
7885 
7886 	auto smw = new ScrollMessageWidget(window);
7887 	smw.addDefaultKeyboardListeners();
7888 	smw.addDefaultWheelListeners();
7889 
7890 	window.loop();
7891 }
7892 
7893 /++
7894 	Bypasses automatic layout for its children, using manual positioning and sizing only.
7895 	While you need to manually position them, you must ensure they are inside the StaticLayout's
7896 	bounding box to avoid undefined behavior.
7897 
7898 	You should almost never use this.
7899 +/
7900 class StaticLayout : Layout {
7901 	///
7902 	this(Widget parent) { super(parent); }
7903 	override void recomputeChildLayout() {
7904 		registerMovement();
7905 		foreach(child; children)
7906 			child.recomputeChildLayout();
7907 	}
7908 }
7909 
7910 /++
7911 	Bypasses automatic positioning when being laid out. It is your responsibility to make
7912 	room for this widget in the parent layout.
7913 
7914 	Its children are laid out normally, unless there is exactly one, in which case it takes
7915 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
7916 	can do that with `padding`).
7917 +/
7918 class StaticPosition : Layout {
7919 	///
7920 	this(Widget parent) { super(parent); }
7921 
7922 	override void recomputeChildLayout() {
7923 		registerMovement();
7924 		if(this.children.length == 1) {
7925 			auto child = children[0];
7926 			child.x = 0;
7927 			child.y = 0;
7928 			child.width = this.width;
7929 			child.height = this.height;
7930 			child.recomputeChildLayout();
7931 		} else
7932 		foreach(child; children)
7933 			child.recomputeChildLayout();
7934 	}
7935 
7936 	alias width = typeof(super).width;
7937 	alias height = typeof(super).height;
7938 
7939 	@property int width(int w) @nogc pure @safe nothrow {
7940 		return this._width = w;
7941 	}
7942 
7943 	@property int height(int w) @nogc pure @safe nothrow {
7944 		return this._height = w;
7945 	}
7946 
7947 }
7948 
7949 /++
7950 	FixedPosition is like [StaticPosition], but its coordinates
7951 	are always relative to the viewport, meaning they do not scroll with
7952 	the parent content.
7953 +/
7954 class FixedPosition : StaticPosition {
7955 	///
7956 	this(Widget parent) { super(parent); }
7957 }
7958 
7959 version(win32_widgets)
7960 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
7961 	if(true) {
7962 		// cmd == 0 = menu, cmd == 1 = accelerator
7963 		if(auto item = idm in Action.mapping) {
7964 			foreach(handler; (*item).triggered)
7965 				handler();
7966 		/*
7967 			auto event = new Event("triggered", *item);
7968 			event.button = idm;
7969 			event.dispatch();
7970 		*/
7971 			return 0;
7972 		}
7973 	}
7974 	if(handle)
7975 	if(auto widgetp = handle in Widget.nativeMapping) {
7976 		(*widgetp).handleWmCommand(cmd, idm);
7977 		return 0;
7978 	}
7979 	return 1;
7980 }
7981 
7982 
7983 ///
7984 class Window : Widget {
7985 	int mouseCaptureCount = 0;
7986 	Widget mouseCapturedBy;
7987 	void captureMouse(Widget byWhom) {
7988 		assert(mouseCapturedBy is null || byWhom is mouseCapturedBy);
7989 		mouseCaptureCount++;
7990 		mouseCapturedBy = byWhom;
7991 		win.grabInput();
7992 	}
7993 	void releaseMouseCapture() {
7994 		mouseCaptureCount--;
7995 		mouseCapturedBy = null;
7996 		win.releaseInputGrab();
7997 	}
7998 
7999 	/++
8000 		Sets the window icon which is often seen in title bars and taskbars.
8001 
8002 		History:
8003 			Added April 5, 2022 (dub v10.8)
8004 	+/
8005 	@property void icon(MemoryImage icon) {
8006 		if(win && icon)
8007 			win.icon = icon;
8008 	}
8009 
8010 	// forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples
8011 	// this does NOT change the icon on the window! That's what the other overload is for
8012 	static @property .icon icon(GenericIcons i) {
8013 		return .icon(i);
8014 	}
8015 
8016 	///
8017 	@scriptable
8018 	@property bool focused() {
8019 		return win.focused;
8020 	}
8021 
8022 	static class Style : Widget.Style {
8023 		override WidgetBackground background() {
8024 			version(custom_widgets)
8025 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8026 			else version(win32_widgets)
8027 				return WidgetBackground(Color.transparent);
8028 			else static assert(0);
8029 		}
8030 	}
8031 	mixin OverrideStyle!Style;
8032 
8033 	/++
8034 		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.
8035 	+/
8036 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8037 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8038 	}
8039 
8040 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8041 		OperatingSystemFont font;
8042 		if(auto vt = WidgetPainter.visualTheme) {
8043 			font = vt.defaultFontCached(96); // FIXME
8044 		}
8045 
8046 		if(font is null) {
8047 			static int defaultHeightCache;
8048 			if(defaultHeightCache == 0) {
8049 				font = new OperatingSystemFont;
8050 				font.loadDefault;
8051 				defaultHeightCache = font.height();// * 5 / 4;
8052 			}
8053 			return defaultHeightCache;
8054 		}
8055 
8056 		return font.height();// * 5 / 4;
8057 	}
8058 
8059 	Widget focusedWidget;
8060 
8061 	private SimpleWindow win_;
8062 
8063 	@property {
8064 		/++
8065 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8066 
8067 			History:
8068 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8069 		+/
8070 		public SimpleWindow win() {
8071 			return win_;
8072 		}
8073 		///
8074 		protected void win(SimpleWindow w) {
8075 			win_ = w;
8076 		}
8077 	}
8078 
8079 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8080 	this(Widget p) {
8081 		tabStop = false;
8082 		super(p);
8083 	}
8084 
8085 	private void actualRedraw() {
8086 		if(recomputeChildLayoutRequired)
8087 			recomputeChildLayoutEntry();
8088 		if(!showing) return;
8089 
8090 		assert(parentWindow !is null);
8091 
8092 		auto w = drawableWindow;
8093 		if(w is null)
8094 			w = parentWindow.win;
8095 
8096 		if(w.closed())
8097 			return;
8098 
8099 		auto ugh = this.parent;
8100 		int lox, loy;
8101 		while(ugh) {
8102 			lox += ugh.x;
8103 			loy += ugh.y;
8104 			ugh = ugh.parent;
8105 		}
8106 		auto painter = w.draw(true);
8107 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8108 	}
8109 
8110 
8111 	private bool skipNextChar = false;
8112 
8113 	/++
8114 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8115 
8116 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8117 	+/
8118 	this(SimpleWindow win) {
8119 
8120 		static if(UsingSimpledisplayX11) {
8121 			win.discardAdditionalConnectionState = &discardXConnectionState;
8122 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8123 		}
8124 
8125 		tabStop = false;
8126 		super(null);
8127 		this.win = win;
8128 
8129 		win.addEventListener((Widget.RedrawEvent) {
8130 			if(win.eventQueued!RecomputeEvent) {
8131 				// writeln("skipping");
8132 				return; // let the recompute event do the actual redraw
8133 			}
8134 			this.actualRedraw();
8135 		});
8136 
8137 		win.addEventListener((Widget.RecomputeEvent) {
8138 			recomputeChildLayoutEntry();
8139 			if(win.eventQueued!RedrawEvent)
8140 				return; // let the queued one do it
8141 			else {
8142 				// writeln("drawing");
8143 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8144 			}
8145 		});
8146 
8147 		this.width = win.width;
8148 		this.height = win.height;
8149 		this.parentWindow = this;
8150 
8151 		win.closeQuery = () {
8152 			if(this.emit!ClosingEvent())
8153 				win.close();
8154 		};
8155 		win.onClosing = () {
8156 			this.emit!ClosedEvent();
8157 		};
8158 
8159 		win.windowResized = (int w, int h) {
8160 			this.width = w;
8161 			this.height = h;
8162 			queueRecomputeChildLayout();
8163 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8164 			//version(win32_widgets)
8165 				//InvalidateRect(hwnd, null, true);
8166 			redraw();
8167 		};
8168 
8169 		win.onFocusChange = (bool getting) {
8170 			if(this.focusedWidget) {
8171 				if(getting) {
8172 					this.focusedWidget.emit!FocusEvent();
8173 					this.focusedWidget.emit!FocusInEvent();
8174 				} else {
8175 					this.focusedWidget.emit!BlurEvent();
8176 					this.focusedWidget.emit!FocusOutEvent();
8177 				}
8178 			}
8179 
8180 			if(getting) {
8181 				this.emit!FocusEvent();
8182 				this.emit!FocusInEvent();
8183 			} else {
8184 				this.emit!BlurEvent();
8185 				this.emit!FocusOutEvent();
8186 			}
8187 		};
8188 
8189 		win.onDpiChanged = {
8190 			this.queueRecomputeChildLayout();
8191 			auto event = new DpiChangedEvent(this);
8192 			event.sendDirectly();
8193 
8194 			privateDpiChanged();
8195 		};
8196 
8197 		win.setEventHandlers(
8198 			(MouseEvent e) {
8199 				dispatchMouseEvent(e);
8200 			},
8201 			(KeyEvent e) {
8202 				//writefln("%x   %s", cast(uint) e.key, e.key);
8203 				dispatchKeyEvent(e);
8204 			},
8205 			(dchar e) {
8206 				if(e == 13) e = 10; // hack?
8207 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8208 				dispatchCharEvent(e);
8209 			},
8210 		);
8211 
8212 		addEventListener("char", (Widget, Event ev) {
8213 			if(skipNextChar) {
8214 				ev.preventDefault();
8215 				skipNextChar = false;
8216 			}
8217 		});
8218 
8219 		version(win32_widgets)
8220 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
8221 			if(hwnd !is this.win.impl.hwnd)
8222 				return 1; // we don't care... pass it on
8223 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
8224 			if(mustReturn)
8225 				return ret;
8226 			return 1; // pass it on
8227 		};
8228 
8229 		if(Window.newWindowCreated)
8230 			Window.newWindowCreated(this);
8231 	}
8232 
8233 	version(custom_widgets)
8234 	override void defaultEventHandler_click(ClickEvent event) {
8235 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
8236 			if(event.target && event.target.tabStop)
8237 				event.target.focus();
8238 		}
8239 	}
8240 
8241 	private static void delegate(Window) newWindowCreated;
8242 
8243 	version(win32_widgets)
8244 	override void paint(WidgetPainter painter) {
8245 		/*
8246 		RECT rect;
8247 		rect.right = this.width;
8248 		rect.bottom = this.height;
8249 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
8250 		*/
8251 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
8252 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
8253 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
8254 		// since the pen is null, to fill the whole space, we need the +1 on both.
8255 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
8256 		SelectObject(painter.impl.hdc, p);
8257 		SelectObject(painter.impl.hdc, b);
8258 	}
8259 	version(custom_widgets)
8260 	override void paint(WidgetPainter painter) {
8261 		auto cs = getComputedStyle();
8262 		painter.fillColor = cs.windowBackgroundColor;
8263 		painter.outlineColor = cs.windowBackgroundColor;
8264 		painter.drawRectangle(Point(0, 0), this.width, this.height);
8265 	}
8266 
8267 
8268 	override void defaultEventHandler_keydown(KeyDownEvent event) {
8269 		Widget _this = event.target;
8270 
8271 		if(event.key == Key.Tab) {
8272 			/* Window tab ordering is a recursive thingy with each group */
8273 
8274 			// FIXME inefficient
8275 			Widget[] helper(Widget p) {
8276 				if(p.hidden)
8277 					return null;
8278 				Widget[] childOrdering;
8279 
8280 				auto children = p.children.dup;
8281 
8282 				while(true) {
8283 					// UIs should be generally small, so gonna brute force it a little
8284 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
8285 
8286 					Widget smallestTab;
8287 					foreach(ref c; children) {
8288 						if(c is null) continue;
8289 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
8290 							smallestTab = c;
8291 							c = null;
8292 						}
8293 					}
8294 					if(smallestTab !is null) {
8295 						if(smallestTab.tabStop && !smallestTab.hidden)
8296 							childOrdering ~= smallestTab;
8297 						if(!smallestTab.hidden)
8298 							childOrdering ~= helper(smallestTab);
8299 					} else
8300 						break;
8301 
8302 				}
8303 
8304 				return childOrdering;
8305 			}
8306 
8307 			Widget[] tabOrdering = helper(this);
8308 
8309 			Widget recipient;
8310 
8311 			if(tabOrdering.length) {
8312 				bool seenThis = false;
8313 				Widget previous;
8314 				foreach(idx, child; tabOrdering) {
8315 					if(child is focusedWidget) {
8316 
8317 						if(event.shiftKey) {
8318 							if(idx == 0)
8319 								recipient = tabOrdering[$-1];
8320 							else
8321 								recipient = tabOrdering[idx - 1];
8322 							break;
8323 						}
8324 
8325 						seenThis = true;
8326 						if(idx + 1 == tabOrdering.length) {
8327 							// we're at the end, either move to the next group
8328 							// or start back over
8329 							recipient = tabOrdering[0];
8330 						}
8331 						continue;
8332 					}
8333 					if(seenThis) {
8334 						recipient = child;
8335 						break;
8336 					}
8337 					previous = child;
8338 				}
8339 			}
8340 
8341 			if(recipient !is null) {
8342 				//  writeln(typeid(recipient));
8343 				recipient.focus();
8344 
8345 				skipNextChar = true;
8346 			}
8347 		}
8348 
8349 		debug if(event.key == Key.F12) {
8350 			if(devTools) {
8351 				devTools.close();
8352 				devTools = null;
8353 			} else {
8354 				devTools = new DevToolWindow(this);
8355 				devTools.show();
8356 			}
8357 		}
8358 	}
8359 
8360 	debug DevToolWindow devTools;
8361 
8362 
8363 	/++
8364 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
8365 
8366 		History:
8367 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
8368 
8369 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
8370 	+/
8371 	this(int width = 500, int height = 500, string title = null) {
8372 		if(title is null) {
8373 			import core.runtime;
8374 			if(Runtime.args.length)
8375 				title = Runtime.args[0];
8376 		}
8377 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus);
8378 
8379 		static if(UsingSimpledisplayX11) {
8380 		///+
8381 		// for input proxy
8382 		auto display = XDisplayConnection.get;
8383 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
8384 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
8385 		XMapWindow(display, inputProxy);
8386 		// writefln("input proxy: 0x%0x", inputProxy);
8387 		this.inputProxy = new SimpleWindow(inputProxy);
8388 
8389 		XEvent lastEvent;
8390 		this.inputProxy.handleNativeEvent = (XEvent ev) {
8391 			lastEvent = ev;
8392 			return 1;
8393 		};
8394 		this.inputProxy.setEventHandlers(
8395 			(MouseEvent e) {
8396 				dispatchMouseEvent(e);
8397 			},
8398 			(KeyEvent e) {
8399 				//writefln("%x   %s", cast(uint) e.key, e.key);
8400 				if(dispatchKeyEvent(e)) {
8401 					// FIXME: i should trap error
8402 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
8403 						auto thing = nw.focusableWindow();
8404 						if(thing && thing.window) {
8405 							lastEvent.xkey.window = thing.window;
8406 							// writeln("sending event ", lastEvent.xkey);
8407 							trapXErrors( {
8408 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
8409 							});
8410 						}
8411 					}
8412 				}
8413 			},
8414 			(dchar e) {
8415 				if(e == 13) e = 10; // hack?
8416 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8417 				dispatchCharEvent(e);
8418 			},
8419 		);
8420 
8421 		this.inputProxy.populateXic();
8422 		// done
8423 		//+/
8424 		}
8425 
8426 
8427 
8428 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
8429 
8430 		this(win);
8431 	}
8432 
8433 	SimpleWindow inputProxy;
8434 
8435 	private SimpleWindow setRequestedInputFocus() {
8436 		return inputProxy;
8437 	}
8438 
8439 	/// ditto
8440 	this(string title, int width = 500, int height = 500) {
8441 		this(width, height, title);
8442 	}
8443 
8444 	///
8445 	@property string title() { return parentWindow.win.title; }
8446 	///
8447 	@property void title(string title) { parentWindow.win.title = title; }
8448 
8449 	///
8450 	@scriptable
8451 	void close() {
8452 		win.close();
8453 		// I synchronize here upon window closing to ensure all child windows
8454 		// get updated too before the event loop. This avoids some random X errors.
8455 		static if(UsingSimpledisplayX11) {
8456 			runInGuiThread( {
8457 				XSync(XDisplayConnection.get, false);
8458 			});
8459 		}
8460 	}
8461 
8462 	bool dispatchKeyEvent(KeyEvent ev) {
8463 		auto wid = focusedWidget;
8464 		if(wid is null)
8465 			wid = this;
8466 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
8467 		event.originalKeyEvent = ev;
8468 		event.key = ev.key;
8469 		event.state = ev.modifierState;
8470 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8471 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8472 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8473 		event.dispatch();
8474 
8475 		return !event.propagationStopped;
8476 	}
8477 
8478 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
8479 	bool dispatchCharEvent(dchar ch) {
8480 		if(focusedWidget) {
8481 			auto event = new CharEvent(focusedWidget, ch);
8482 			event.dispatch();
8483 			return !event.propagationStopped;
8484 		}
8485 		return true;
8486 	}
8487 
8488 	Widget mouseLastOver;
8489 	Widget mouseLastDownOn;
8490 	bool lastWasDoubleClick;
8491 	bool dispatchMouseEvent(MouseEvent ev) {
8492 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
8493 		auto ele = eleR.widget;
8494 
8495 		auto captureEle = ele;
8496 
8497 		if(mouseCapturedBy !is null) {
8498 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
8499 				captureEle = mouseCapturedBy;
8500 		}
8501 
8502 		// a hack to get it relative to the widget.
8503 		eleR.x = ev.x;
8504 		eleR.y = ev.y;
8505 		auto pain = captureEle;
8506 		while(pain) {
8507 			eleR.x -= pain.x;
8508 			eleR.y -= pain.y;
8509 			pain.addScrollPosition(eleR.x, eleR.y);
8510 			pain = pain.parent;
8511 		}
8512 
8513 		void populateMouseEventBase(MouseEventBase event) {
8514 			event.button = ev.button;
8515 			event.buttonLinear = ev.buttonLinear;
8516 			event.state = ev.modifierState;
8517 			event.clientX = eleR.x;
8518 			event.clientY = eleR.y;
8519 
8520 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8521 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8522 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8523 		}
8524 
8525 		if(ev.type == MouseEventType.buttonPressed) {
8526 			{
8527 				auto event = new MouseDownEvent(captureEle);
8528 				populateMouseEventBase(event);
8529 				event.dispatch();
8530 			}
8531 
8532 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
8533 				auto event = new DoubleClickEvent(captureEle);
8534 				populateMouseEventBase(event);
8535 				event.dispatch();
8536 				lastWasDoubleClick = ev.doubleClick;
8537 			} else {
8538 				lastWasDoubleClick = false;
8539 			}
8540 
8541 			mouseLastDownOn = ele;
8542 		} else if(ev.type == MouseEventType.buttonReleased) {
8543 			{
8544 				auto event = new MouseUpEvent(captureEle);
8545 				populateMouseEventBase(event);
8546 				event.dispatch();
8547 			}
8548 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
8549 				auto event = new ClickEvent(captureEle);
8550 				populateMouseEventBase(event);
8551 				event.dispatch();
8552 			}
8553 		} else if(ev.type == MouseEventType.motion) {
8554 			// motion
8555 			{
8556 				auto event = new MouseMoveEvent(captureEle);
8557 				populateMouseEventBase(event); // fills in button which is meaningless but meh
8558 				event.dispatch();
8559 			}
8560 
8561 			if(mouseLastOver !is ele) {
8562 				if(ele !is null) {
8563 					if(!isAParentOf(ele, mouseLastOver)) {
8564 						ele.setDynamicState(DynamicState.hover, true);
8565 						auto event = new MouseEnterEvent(ele);
8566 						event.relatedTarget = mouseLastOver;
8567 						event.sendDirectly();
8568 
8569 						ele.useStyleProperties((scope Widget.Style s) {
8570 							ele.parentWindow.win.cursor = s.cursor;
8571 						});
8572 					}
8573 				}
8574 
8575 				if(mouseLastOver !is null) {
8576 					if(!isAParentOf(mouseLastOver, ele)) {
8577 						mouseLastOver.setDynamicState(DynamicState.hover, false);
8578 						auto event = new MouseLeaveEvent(mouseLastOver);
8579 						event.relatedTarget = ele;
8580 						event.sendDirectly();
8581 					}
8582 				}
8583 
8584 				if(ele !is null) {
8585 					auto event = new MouseOverEvent(ele);
8586 					event.relatedTarget = mouseLastOver;
8587 					event.dispatch();
8588 				}
8589 
8590 				if(mouseLastOver !is null) {
8591 					auto event = new MouseOutEvent(mouseLastOver);
8592 					event.relatedTarget = ele;
8593 					event.dispatch();
8594 				}
8595 
8596 				mouseLastOver = ele;
8597 			}
8598 		}
8599 
8600 		return true; // FIXME: the event default prevented?
8601 	}
8602 
8603 	/++
8604 		Shows the window and runs the application event loop.
8605 
8606 		Blocks until this window is closed.
8607 
8608 		Bugs:
8609 
8610 		$(PITFALL
8611 			You should always have one event loop live for your application.
8612 			If you make two windows in sequence, the second call to loop (or
8613 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
8614 			might fail:
8615 
8616 			---
8617 			// don't do this!
8618 			auto window = new Window();
8619 			window.loop();
8620 
8621 			// or new Window or new MainWindow, all the same
8622 			auto window2 = new SimpleWindow();
8623 			window2.eventLoop(0); // problematic! might crash
8624 			---
8625 
8626 			simpledisplay's current implementation assumes that final cleanup is
8627 			done when the event loop refcount reaches zero. So after the first
8628 			eventLoop returns, when there isn't already another one active, it assumes
8629 			the program will exit soon and cleans up.
8630 
8631 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
8632 			it eventually, but in the mean time, there's an easy solution:
8633 
8634 			---
8635 			// do this
8636 			EventLoop mainEventLoop = EventLoop.get; // just add this line
8637 
8638 			auto window = new Window();
8639 			window.loop();
8640 
8641 			// or any other type of Window etc.
8642 			auto window2 = new Window();
8643 			window2.loop(); // perfectly fine since mainEventLoop still alive
8644 			---
8645 
8646 			By adding a top-level reference to the event loop, it ensures the final cleanup
8647 			is not performed until it goes out of scope too, letting the individual window loops
8648 			work without trouble despite the bug.
8649 		)
8650 
8651 		History:
8652 			The [BlockingMode] parameter was added on December 8, 2021.
8653 			The default behavior is to block until the application quits
8654 			(so all windows have been closed), unless another minigui or
8655 			simpledisplay event loop is already running, in which case it
8656 			will block until this window closes specifically.
8657 	+/
8658 	@scriptable
8659 	void loop(BlockingMode bm = BlockingMode.automatic) {
8660 		if(win.closed)
8661 			return; // otherwise show will throw
8662 		show();
8663 		win.eventLoopWithBlockingMode(bm, 0);
8664 	}
8665 
8666 	private bool firstShow = true;
8667 
8668 	@scriptable
8669 	override void show() {
8670 		bool rd = false;
8671 		if(firstShow) {
8672 			firstShow = false;
8673 			queueRecomputeChildLayout();
8674 			auto f = getFirstFocusable(this); // FIXME: autofocus?
8675 			if(f)
8676 				f.focus();
8677 			redraw();
8678 		}
8679 		win.show();
8680 		super.show();
8681 	}
8682 	@scriptable
8683 	override void hide() {
8684 		win.hide();
8685 		super.hide();
8686 	}
8687 
8688 	static Widget getFirstFocusable(Widget start) {
8689 		if(start is null)
8690 			return null;
8691 
8692 		foreach(widget; &start.focusableWidgets) {
8693 			return widget;
8694 		}
8695 
8696 		return null;
8697 	}
8698 
8699 	static Widget getLastFocusable(Widget start) {
8700 		if(start is null)
8701 			return null;
8702 
8703 		Widget last;
8704 		foreach(widget; &start.focusableWidgets) {
8705 			last = widget;
8706 		}
8707 
8708 		return last;
8709 	}
8710 
8711 
8712 	mixin Emits!ClosingEvent;
8713 	mixin Emits!ClosedEvent;
8714 }
8715 
8716 /++
8717 	History:
8718 		Added January 12, 2022
8719 +/
8720 class DpiChangedEvent : Event {
8721 	enum EventString = "dpichanged";
8722 
8723 	this(Widget target) {
8724 		super(EventString, target);
8725 	}
8726 }
8727 
8728 debug private class DevToolWindow : Window {
8729 	Window p;
8730 
8731 	TextEdit parentList;
8732 	TextEdit logWindow;
8733 	TextLabel clickX, clickY;
8734 
8735 	this(Window p) {
8736 		this.p = p;
8737 		super(400, 300, "Developer Toolbox");
8738 
8739 		logWindow = new TextEdit(this);
8740 		parentList = new TextEdit(this);
8741 
8742 		auto hl = new HorizontalLayout(this);
8743 		clickX = new TextLabel("", TextAlignment.Right, hl);
8744 		clickY = new TextLabel("", TextAlignment.Right, hl);
8745 
8746 		parentListeners ~= p.addEventListener("*", (Event ev) {
8747 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
8748 		});
8749 
8750 		parentListeners ~= p.addEventListener((ClickEvent ev) {
8751 			auto s = ev.srcElement;
8752 
8753 			string list;
8754 
8755 			void addInfo(Widget s) {
8756 				list ~= s.toString();
8757 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
8758 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
8759 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
8760 				list ~= "\n\theight: " ~ toInternal!string(s.height);
8761 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
8762 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
8763 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
8764 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
8765 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
8766 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
8767 			}
8768 
8769 			addInfo(s);
8770 
8771 			s = s.parent;
8772 			while(s) {
8773 				list ~= "\n";
8774 				addInfo(s);
8775 				s = s.parent;
8776 			}
8777 			parentList.content = list;
8778 
8779 			clickX.label = toInternal!string(ev.clientX);
8780 			clickY.label = toInternal!string(ev.clientY);
8781 		});
8782 	}
8783 
8784 	EventListener[] parentListeners;
8785 
8786 	override void close() {
8787 		assert(p !is null);
8788 		foreach(p; parentListeners)
8789 			p.disconnect();
8790 		parentListeners = null;
8791 		p.devTools = null;
8792 		p = null;
8793 		super.close();
8794 	}
8795 
8796 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
8797 		if(ev.key == Key.F12) {
8798 			this.close();
8799 			if(p)
8800 				p.devTools = null;
8801 		} else {
8802 			super.defaultEventHandler_keydown(ev);
8803 		}
8804 	}
8805 
8806 	void log(T...)(T t) {
8807 		string str;
8808 		import std.conv;
8809 		foreach(i; t)
8810 			str ~= to!string(i);
8811 		str ~= "\n";
8812 		logWindow.addText(str);
8813 
8814 		//version(custom_widgets)
8815 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
8816 	}
8817 }
8818 
8819 /++
8820 	A dialog is a transient window that intends to get information from
8821 	the user before being dismissed.
8822 +/
8823 abstract class Dialog : Window {
8824 	///
8825 	this(int width, int height, string title = null) {
8826 		super(width, height, title);
8827 	}
8828 
8829 	///
8830 	abstract void OK();
8831 
8832 	///
8833 	void Cancel() {
8834 		this.close();
8835 	}
8836 }
8837 
8838 /++
8839 	A custom widget similar to the HTML5 <details> tag.
8840 +/
8841 version(none)
8842 class DetailsView : Widget {
8843 
8844 }
8845 
8846 // FIXME: maybe i should expose the other list views Windows offers too
8847 
8848 /++
8849 	A TableView is a widget made to display a table of data strings.
8850 
8851 
8852 	Future_Directions:
8853 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
8854 
8855 		I will add a selection changed event at some point, as well as item clicked events.
8856 	History:
8857 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
8858 	See_Also:
8859 		[ListWidget] which displays a list of strings without additional columns.
8860 +/
8861 class TableView : Widget {
8862 	/++
8863 
8864 	+/
8865 	this(Widget parent) {
8866 		super(parent);
8867 
8868 		version(win32_widgets) {
8869 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
8870 		} else version(custom_widgets) {
8871 			auto smw = new ScrollMessageWidget(this);
8872 			smw.addDefaultKeyboardListeners();
8873 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
8874 			tvwi = new TableViewWidgetInner(this, smw);
8875 		}
8876 	}
8877 
8878 	// FIXME: auto-size columns on double click of header thing like in Windows
8879 	// it need only make the currently displayed things fit well.
8880 
8881 
8882 	private ColumnInfo[] columns;
8883 	private int itemCount;
8884 
8885 	version(custom_widgets) private {
8886 		TableViewWidgetInner tvwi;
8887 	}
8888 
8889 	/// Passed to [setColumnInfo]
8890 	static struct ColumnInfo {
8891 		const(char)[] name; /// the name displayed in the header
8892 		/++
8893 			The default width, in pixels. As a special case, you can set this to -1
8894 			if you want the system to try to automatically size the width to fit visible
8895 			content. If it can't, it will try to pick a sensible default size.
8896 
8897 			Any other negative value is not allowed and may lead to unpredictable results.
8898 
8899 			History:
8900 				The -1 behavior was specified on December 3, 2021. It actually worked before
8901 				anyway on Win32 but now it is a formal feature with partial Linux support.
8902 
8903 			Bugs:
8904 				It doesn't actually attempt to calculate a best-fit width on Linux as of
8905 				December 3, 2021. I do plan to fix this in the future, but Windows is the
8906 				priority right now. At least it doesn't break things when you use it now.
8907 		+/
8908 		int width;
8909 
8910 		/++
8911 			Alignment of the text in the cell. Applies to the header as well as all data in this
8912 			column.
8913 
8914 			Bugs:
8915 				On Windows, the first column ignores this member and is always left aligned.
8916 				You can work around this by inserting a dummy first column with width = 0
8917 				then putting your actual data in the second column, which does respect the
8918 				alignment.
8919 
8920 				This is a quirk of the operating system's implementation going back a very
8921 				long time and is unlikely to ever be fixed.
8922 		+/
8923 		TextAlignment alignment;
8924 
8925 		/++
8926 			After all the pixel widths have been assigned, any left over
8927 			space is divided up among all columns and distributed to according
8928 			to the widthPercent field.
8929 
8930 
8931 			For example, if you have two fields, both with width 50 and one with
8932 			widthPercent of 25 and the other with widthPercent of 75, and the
8933 			container is 200 pixels wide, first both get their width of 50.
8934 			then the 100 remaining pixels are split up, so the one gets a total
8935 			of 75 pixels and the other gets a total of 125.
8936 
8937 			This is automatically applied as the window is resized.
8938 
8939 			If there is not enough space - that is, when a horizontal scrollbar
8940 			needs to appear - there are 0 pixels divided up, and thus everyone
8941 			gets 0. This can cause a column to shrink out of proportion when
8942 			passing the scroll threshold.
8943 
8944 			It is important to still set a fixed width (that is, to populate the
8945 			`width` field) even if you use the percents because that will be the
8946 			default minimum in the event of a scroll bar appearing.
8947 
8948 			The percents total in the column can never exceed 100 or be less than 0.
8949 			Doing this will trigger an assert error.
8950 
8951 			Implementation note:
8952 
8953 			Please note that percentages are only recalculated 1) upon original
8954 			construction and 2) upon resizing the control. If the user adjusts the
8955 			width of a column, the percentage items will not be updated.
8956 
8957 			On the other hand, if the user adjusts the width of a percentage column
8958 			then resizes the window, it is recalculated, meaning their hand adjustment
8959 			is discarded. This specific behavior may change in the future as it is
8960 			arguably a bug, but I'm not certain yet.
8961 
8962 			History:
8963 				Added November 10, 2021 (dub v10.4)
8964 		+/
8965 		int widthPercent;
8966 
8967 
8968 		private int calculatedWidth;
8969 	}
8970 	/++
8971 		Sets the number of columns along with information about the headers.
8972 
8973 		Please note: on Windows, the first column ignores your alignment preference
8974 		and is always left aligned.
8975 	+/
8976 	void setColumnInfo(ColumnInfo[] columns...) {
8977 
8978 		foreach(ref c; columns) {
8979 			c.name = c.name.idup;
8980 		}
8981 		this.columns = columns.dup;
8982 
8983 		updateCalculatedWidth(false);
8984 
8985 		version(custom_widgets) {
8986 			tvwi.header.updateHeaders();
8987 			tvwi.updateScrolls();
8988 		} else version(win32_widgets)
8989 		foreach(i, column; this.columns) {
8990 			LVCOLUMN lvColumn;
8991 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
8992 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
8993 
8994 			auto bfr = WCharzBuffer(column.name);
8995 			lvColumn.pszText = bfr.ptr;
8996 
8997 			if(column.alignment & TextAlignment.Center)
8998 				lvColumn.fmt = LVCFMT_CENTER;
8999 			else if(column.alignment & TextAlignment.Right)
9000 				lvColumn.fmt = LVCFMT_RIGHT;
9001 			else
9002 				lvColumn.fmt = LVCFMT_LEFT;
9003 
9004 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
9005 				throw new WindowsApiException("Insert Column Fail", GetLastError());
9006 		}
9007 	}
9008 
9009 	private int getActualSetSize(size_t i, bool askWindows) {
9010 		version(win32_widgets)
9011 			if(askWindows)
9012 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
9013 		auto w = columns[i].width;
9014 		if(w == -1)
9015 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9016 		return w;
9017 	}
9018 
9019 	private void updateCalculatedWidth(bool informWindows) {
9020 		int padding;
9021 		version(win32_widgets)
9022 			padding = 4;
9023 		int remaining = this.width;
9024 		foreach(i, column; columns)
9025 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9026 		remaining -= padding;
9027 		if(remaining < 0)
9028 			remaining = 0;
9029 
9030 		int percentTotal;
9031 		foreach(i, ref column; columns) {
9032 			percentTotal += column.widthPercent;
9033 
9034 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9035 
9036 			column.calculatedWidth = c;
9037 
9038 			version(win32_widgets)
9039 			if(informWindows)
9040 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9041 		}
9042 
9043 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9044 		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).");
9045 
9046 
9047 	}
9048 
9049 	override void registerMovement() {
9050 		super.registerMovement();
9051 
9052 		updateCalculatedWidth(true);
9053 	}
9054 
9055 	/++
9056 		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.
9057 	+/
9058 	void setItemCount(int count) {
9059 		this.itemCount = count;
9060 		version(custom_widgets) {
9061 			tvwi.updateScrolls();
9062 			redraw();
9063 		} else version(win32_widgets) {
9064 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9065 		}
9066 	}
9067 
9068 	/++
9069 		Clears all items;
9070 	+/
9071 	void clear() {
9072 		this.itemCount = 0;
9073 		this.columns = null;
9074 		version(custom_widgets) {
9075 			tvwi.header.updateHeaders();
9076 			tvwi.updateScrolls();
9077 			redraw();
9078 		} else version(win32_widgets) {
9079 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9080 		}
9081 	}
9082 
9083 	/+
9084 	version(win32_widgets)
9085 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9086 		auto itemId = dis.itemID;
9087 		auto hdc = dis.hDC;
9088 		auto rect = dis.rcItem;
9089 		switch(dis.itemAction) {
9090 			case ODA_DRAWENTIRE:
9091 
9092 				// FIXME: do other items
9093 				// FIXME: do the focus rectangle i guess
9094 				// FIXME: alignment
9095 				// FIXME: column width
9096 				// FIXME: padding left
9097 				// FIXME: check dpi scaling
9098 				// FIXME: don't owner draw unless it is necessary.
9099 
9100 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9101 				RECT itemRect;
9102 				itemRect.top = 1; // subitem idx, 1-based
9103 				itemRect.left = LVIR_BOUNDS;
9104 
9105 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9106 				itemRect.left += padding;
9107 
9108 				getData(itemId, 0, (in char[] data) {
9109 					auto wdata = WCharzBuffer(data);
9110 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9111 
9112 				});
9113 			goto case;
9114 			case ODA_FOCUS:
9115 				if(dis.itemState & ODS_FOCUS)
9116 					DrawFocusRect(hdc, &rect);
9117 			break;
9118 			case ODA_SELECT:
9119 				// itemState & ODS_SELECTED
9120 			break;
9121 			default:
9122 		}
9123 		return 1;
9124 	}
9125 	+/
9126 
9127 	version(win32_widgets) {
9128 		CellStyle last;
9129 		COLORREF defaultColor;
9130 		COLORREF defaultBackground;
9131 	}
9132 
9133 	version(win32_widgets)
9134 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9135 		switch(code) {
9136 			case NM_CUSTOMDRAW:
9137 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9138 				switch(s.nmcd.dwDrawStage) {
9139 					case CDDS_PREPAINT:
9140 						if(getCellStyle is null)
9141 							return 0;
9142 
9143 						mustReturn = true;
9144 						return CDRF_NOTIFYITEMDRAW;
9145 					case CDDS_ITEMPREPAINT:
9146 						mustReturn = true;
9147 						return CDRF_NOTIFYSUBITEMDRAW;
9148 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9149 						mustReturn = true;
9150 
9151 						if(getCellStyle is null) // this SHOULD never happen...
9152 							return 0;
9153 
9154 						if(s.iSubItem == 0) {
9155 							// Windows resets it per row so we'll use item 0 as a chance
9156 							// to capture these for later
9157 							defaultColor = s.clrText;
9158 							defaultBackground = s.clrTextBk;
9159 						}
9160 
9161 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9162 						// if no special style and no reset needed...
9163 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9164 							return 0; // allow default processing to continue
9165 
9166 						last = style;
9167 
9168 						// might still need to reset or use the preference.
9169 
9170 						if(style.flags & CellStyle.Flags.textColorSet)
9171 							s.clrText = style.textColor.asWindowsColorRef;
9172 						else
9173 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
9174 						if(style.flags & CellStyle.Flags.backgroundColorSet)
9175 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
9176 						else
9177 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
9178 
9179 						return CDRF_NEWFONT;
9180 					default:
9181 						return 0;
9182 
9183 				}
9184 			case NM_RETURN: // no need since i subclass keydown
9185 			break;
9186 			case LVN_COLUMNCLICK:
9187 				auto info = cast(LPNMLISTVIEW) hdr;
9188 				this.emit!HeaderClickedEvent(info.iSubItem);
9189 			break;
9190 			case NM_CLICK:
9191 			case NM_DBLCLK:
9192 			case NM_RCLICK:
9193 			case NM_RDBLCLK:
9194 				// the item/subitem is set here and that can be a useful notification
9195 				// even beyond the normal click notification
9196 			break;
9197 			case LVN_GETDISPINFO:
9198 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
9199 				if(info.item.mask & LVIF_TEXT) {
9200 					if(getData) {
9201 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
9202 							auto bfr = WCharzBuffer(dataReceived);
9203 							auto len = info.item.cchTextMax;
9204 							if(bfr.length < len)
9205 								len = cast(typeof(len)) bfr.length;
9206 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
9207 							info.item.pszText[len] = 0;
9208 						});
9209 					} else {
9210 						info.item.pszText[0] = 0;
9211 					}
9212 					//info.item.iItem
9213 					//if(info.item.iSubItem)
9214 				}
9215 			break;
9216 			default:
9217 		}
9218 		return 0;
9219 	}
9220 
9221 	override bool encapsulatedChildren() {
9222 		return true;
9223 	}
9224 
9225 	/++
9226 		Informs the control that content has changed.
9227 
9228 		History:
9229 			Added November 10, 2021 (dub v10.4)
9230 	+/
9231 	void update() {
9232 		version(custom_widgets)
9233 			redraw();
9234 		else {
9235 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
9236 			UpdateWindow(hwnd);
9237 		}
9238 
9239 
9240 	}
9241 
9242 	/++
9243 		Called by the system to request the text content of an individual cell. You
9244 		should pass the text into the provided `sink` delegate. This function will be
9245 		called for each visible cell as-needed when drawing.
9246 	+/
9247 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
9248 
9249 	/++
9250 		Available per-cell style customization options. Use one of the constructors
9251 		provided to set the values conveniently, or default construct it and set individual
9252 		values yourself. Just remember to set the `flags` so your values are actually used.
9253 		If the flag isn't set, the field is ignored and the system default is used instead.
9254 
9255 		This is returned by the [getCellStyle] delegate.
9256 
9257 		Examples:
9258 			---
9259 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
9260 			auto table = new TableView(window);
9261 			// snip: you would set up columns here
9262 
9263 			// this is how you provide data to the table view class
9264 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
9265 				import std.conv;
9266 				sink(to!string(my_data[row][column]));
9267 			};
9268 
9269 			// and this is how you customize the colors
9270 			table.getCellStyle = delegate(int row, int column) {
9271 				return (my_data[row][column] < 0) ?
9272 					TableView.CellStyle(Color.red); // make negative numbers red
9273 					: TableView.CellStyle.init; // leave the rest alone
9274 			};
9275 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
9276 			---
9277 
9278 		History:
9279 			Added November 27, 2021 (dub v10.4)
9280 	+/
9281 	struct CellStyle {
9282 		/// 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.
9283 		this(Color textColor) {
9284 			this.textColor = textColor;
9285 			this.flags |= Flags.textColorSet;
9286 		}
9287 		/// Sets a custom text and background color.
9288 		this(Color textColor, Color backgroundColor) {
9289 			this.textColor = textColor;
9290 			this.backgroundColor = backgroundColor;
9291 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
9292 		}
9293 
9294 		Color textColor;
9295 		Color backgroundColor;
9296 		int flags; /// bitmask of [Flags]
9297 		/// available options to combine into [flags]
9298 		enum Flags {
9299 			textColorSet = 1 << 0,
9300 			backgroundColorSet = 1 << 1,
9301 		}
9302 	}
9303 	/++
9304 		Companion delegate to [getData] that allows you to custom style each
9305 		cell of the table.
9306 
9307 		Returns:
9308 			A [CellStyle] structure that describes the desired style for the
9309 			given cell. `return CellStyle.init` if you want the default style.
9310 
9311 		History:
9312 			Added November 27, 2021 (dub v10.4)
9313 	+/
9314 	CellStyle delegate(int row, int column) getCellStyle;
9315 
9316 	// i want to be able to do things like draw little colored things to show red for negative numbers
9317 	// or background color indicators or even in-cell charts
9318 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
9319 
9320 	/++
9321 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
9322 	+/
9323 	mixin Emits!HeaderClickedEvent;
9324 }
9325 
9326 /++
9327 	This is emitted by the [TableView] when a user clicks on a column header.
9328 
9329 	Its member `columnIndex` has the zero-based index of the column that was clicked.
9330 
9331 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
9332 
9333 	History:
9334 		Added November 27, 2021 (dub v10.4)
9335 +/
9336 class HeaderClickedEvent : Event {
9337 	enum EventString = "HeaderClicked";
9338 	this(Widget target, int columnIndex) {
9339 		this.columnIndex = columnIndex;
9340 		super(EventString, target);
9341 	}
9342 
9343 	/// The index of the column
9344 	int columnIndex;
9345 
9346 	///
9347 	override @property int intValue() {
9348 		return columnIndex;
9349 	}
9350 }
9351 
9352 version(custom_widgets)
9353 private class TableViewWidgetInner : Widget {
9354 
9355 // wrap this thing in a ScrollMessageWidget
9356 
9357 	TableView tvw;
9358 	ScrollMessageWidget smw;
9359 	HeaderWidget header;
9360 
9361 	this(TableView tvw, ScrollMessageWidget smw) {
9362 		this.tvw = tvw;
9363 		this.smw = smw;
9364 		super(smw);
9365 
9366 		this.tabStop = true;
9367 
9368 		header = new HeaderWidget(this, smw.getHeader());
9369 
9370 		smw.addEventListener("scroll", () {
9371 			this.redraw();
9372 			header.redraw();
9373 		});
9374 
9375 
9376 		// I need headers outside the scroll area but rendered on the same line as the up arrow
9377 		// FIXME: add a fixed header to the SMW
9378 	}
9379 
9380 	enum padding = 3;
9381 
9382 	void updateScrolls() {
9383 		int w;
9384 		foreach(idx, column; tvw.columns) {
9385 			if(column.width == 0) continue;
9386 			w += tvw.getActualSetSize(idx, false);// + padding;
9387 		}
9388 		smw.setTotalArea(w, tvw.itemCount);
9389 		columnsWidth = w;
9390 	}
9391 
9392 	private int columnsWidth;
9393 
9394 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
9395 
9396 	override void registerMovement() {
9397 		super.registerMovement();
9398 		// FIXME: actual column width. it might need to be done per-pixel instead of per-colum
9399 		smw.setViewableArea(this.width, this.height / lh);
9400 	}
9401 
9402 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
9403 		int x;
9404 		int y;
9405 
9406 		int row = smw.position.y;
9407 
9408 		foreach(lol; 0 .. this.height / lh) {
9409 			if(row >= tvw.itemCount)
9410 				break;
9411 			x = 0;
9412 			foreach(columnNumber, column; tvw.columns) {
9413 				auto x2 = x + column.calculatedWidth;
9414 				auto smwx = smw.position.x;
9415 
9416 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
9417 					auto startX = x;
9418 					auto endX = x + column.calculatedWidth;
9419 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
9420 						case TextAlignment.Left: startX += padding; break;
9421 						case TextAlignment.Center: startX += padding; endX -= padding; break;
9422 						case TextAlignment.Right: endX -= padding; break;
9423 						default: /* broken */ break;
9424 					}
9425 					if(column.width != 0) // no point drawing an invisible column
9426 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
9427 						// auto clip = painter.setClipRectangle(
9428 
9429 						void dotext(WidgetPainter painter) {
9430 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
9431 						}
9432 
9433 						if(tvw.getCellStyle !is null) {
9434 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
9435 
9436 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
9437 								auto tempPainter = painter;
9438 								tempPainter.fillColor = style.backgroundColor;
9439 								tempPainter.outlineColor = style.backgroundColor;
9440 
9441 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
9442 									Point(endX - smw.position.x, y + lh));
9443 							}
9444 							auto tempPainter = painter;
9445 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
9446 								tempPainter.outlineColor = style.textColor;
9447 
9448 							dotext(tempPainter);
9449 						} else {
9450 							dotext(painter);
9451 						}
9452 					});
9453 				}
9454 
9455 				x += column.calculatedWidth;
9456 			}
9457 			row++;
9458 			y += lh;
9459 		}
9460 		return bounds;
9461 	}
9462 
9463 	static class Style : Widget.Style {
9464 		override WidgetBackground background() {
9465 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
9466 		}
9467 	}
9468 	mixin OverrideStyle!Style;
9469 
9470 	private static class HeaderWidget : Widget {
9471 		this(TableViewWidgetInner tvw, Widget parent) {
9472 			super(parent);
9473 			this.tvw = tvw;
9474 
9475 			this.remainder = new Button("", this);
9476 
9477 			this.addEventListener((scope ClickEvent ev) {
9478 				int header = -1;
9479 				foreach(idx, child; this.children[1 .. $]) {
9480 					if(child is ev.target) {
9481 						header = cast(int) idx;
9482 						break;
9483 					}
9484 				}
9485 
9486 				if(header != -1) {
9487 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
9488 					hce.dispatch();
9489 				}
9490 
9491 			});
9492 		}
9493 
9494 		void updateHeaders() {
9495 			foreach(child; children[1 .. $])
9496 				child.removeWidget();
9497 
9498 			foreach(column; tvw.tvw.columns) {
9499 				// the cast is ok because I dup it above, just the type is never changed.
9500 				// all this is private so it should never get messed up.
9501 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
9502 			}
9503 		}
9504 
9505 		Button remainder;
9506 		TableViewWidgetInner tvw;
9507 
9508 		override void recomputeChildLayout() {
9509 			registerMovement();
9510 			int pos;
9511 			foreach(idx, child; children[1 .. $]) {
9512 				if(idx >= tvw.tvw.columns.length)
9513 					continue;
9514 				child.x = pos;
9515 				child.y = 0;
9516 				child.width = tvw.tvw.columns[idx].calculatedWidth;
9517 				child.height = scaleWithDpi(16);// this.height;
9518 				pos += child.width;
9519 
9520 				child.recomputeChildLayout();
9521 			}
9522 
9523 			if(remainder is null)
9524 				return;
9525 
9526 			remainder.x = pos;
9527 			remainder.y = 0;
9528 			if(pos < this.width)
9529 				remainder.width = this.width - pos;// + 4;
9530 			else
9531 				remainder.width = 0;
9532 			remainder.height = scaleWithDpi(16);
9533 
9534 			remainder.recomputeChildLayout();
9535 		}
9536 
9537 		// for the scrollable children mixin
9538 		Point scrollOrigin() {
9539 			return Point(tvw.smw.position.x, 0);
9540 		}
9541 		void paintFrameAndBackground(WidgetPainter painter) { }
9542 
9543 		mixin ScrollableChildren;
9544 	}
9545 }
9546 
9547 /+
9548 
9549 // given struct / array / number / string / etc, make it viewable and editable
9550 class DataViewerWidget : Widget {
9551 
9552 }
9553 +/
9554 
9555 /++
9556 	A line edit box with an associated label.
9557 
9558 	History:
9559 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
9560 
9561 		```
9562 		Old: ________
9563 
9564 		New:
9565 		____________
9566 		```
9567 
9568 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
9569 
9570 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
9571 		horizontal label but left aligned. You may also consider a [GridLayout].
9572 +/
9573 alias LabeledLineEdit = Labeled!LineEdit;
9574 
9575 private int widthThatWouldFitChildLabels(Widget w) {
9576 	if(w is null)
9577 		return 0;
9578 
9579 	int max;
9580 
9581 	if(auto label = cast(TextLabel) w) {
9582 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
9583 	} else {
9584 		foreach(child; w.children) {
9585 			max = mymax(max, widthThatWouldFitChildLabels(child));
9586 		}
9587 	}
9588 
9589 	return max;
9590 }
9591 
9592 /++
9593 	History:
9594 		Added May 19, 2021
9595 +/
9596 class Labeled(T) : Widget {
9597 	///
9598 	this(string label, Widget parent) {
9599 		super(parent);
9600 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
9601 	}
9602 
9603 	/++
9604 		History:
9605 			The alignment parameter was added May 17, 2021
9606 	+/
9607 	this(string label, TextAlignment alignment, Widget parent) {
9608 		super(parent);
9609 		initialize!HorizontalLayout(label, alignment, parent);
9610 	}
9611 
9612 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
9613 		tabStop = false;
9614 		horizontal = is(L == HorizontalLayout);
9615 		auto hl = new L(this);
9616 		if(horizontal) {
9617 			static class SpecialTextLabel : TextLabel {
9618 				Widget outerParent;
9619 
9620 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
9621 					this.outerParent = outerParent;
9622 					super(label, alignment, parent);
9623 				}
9624 
9625 				override int flexBasisWidth() {
9626 					return widthThatWouldFitChildLabels(outerParent);
9627 				}
9628 				/+
9629 				override int widthShrinkiness() { return 0; }
9630 				override int widthStretchiness() { return 1; }
9631 				+/
9632 
9633 				override int paddingRight() { return 6; }
9634 				override int paddingLeft() { return 9; }
9635 
9636 				override int paddingTop() { return 3; }
9637 			}
9638 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
9639 		} else
9640 			this.label = new TextLabel(label, alignment, hl);
9641 		this.lineEdit = new T(hl);
9642 
9643 		this.label.labelFor = this.lineEdit;
9644 	}
9645 
9646 	private bool horizontal;
9647 
9648 	TextLabel label; ///
9649 	T lineEdit; ///
9650 
9651 	override int flexBasisWidth() { return 250; }
9652 	override int widthShrinkiness() { return 1; }
9653 
9654 	override int minHeight() {
9655 		return this.children[0].minHeight;
9656 	}
9657 	override int maxHeight() { return minHeight(); }
9658 	override int marginTop() { return 4; }
9659 	override int marginBottom() { return 4; }
9660 
9661 	// FIXME: i should prolly call it value as well as content tbh
9662 
9663 	///
9664 	@property string content() {
9665 		return lineEdit.content;
9666 	}
9667 	///
9668 	@property void content(string c) {
9669 		return lineEdit.content(c);
9670 	}
9671 
9672 	///
9673 	void selectAll() {
9674 		lineEdit.selectAll();
9675 	}
9676 
9677 	override void focus() {
9678 		lineEdit.focus();
9679 	}
9680 }
9681 
9682 /++
9683 	A labeled password edit.
9684 
9685 	History:
9686 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
9687 
9688 		The default parameters for the constructors were also removed on May 19, 2021
9689 +/
9690 alias LabeledPasswordEdit = Labeled!PasswordEdit;
9691 
9692 private string toMenuLabel(string s) {
9693 	string n;
9694 	n.reserve(s.length);
9695 	foreach(c; s)
9696 		if(c == '_')
9697 			n ~= ' ';
9698 		else
9699 			n ~= c;
9700 	return n;
9701 }
9702 
9703 private void autoExceptionHandler(Exception e) {
9704 	messageBox(e.msg);
9705 }
9706 
9707 private void delegate() makeAutomaticHandler(alias fn, T)(T t) {
9708 	static if(is(T : void delegate())) {
9709 		return () {
9710 			try
9711 				t();
9712 			catch(Exception e)
9713 				autoExceptionHandler(e);
9714 		};
9715 	} else static if(is(typeof(fn) Params == __parameters)) {
9716 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
9717 			return () {
9718 				void onOK(string s) {
9719 					member = s;
9720 					try
9721 						t(Params[0](s));
9722 					catch(Exception e)
9723 						autoExceptionHandler(e);
9724 				}
9725 
9726 				if(
9727 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
9728 					|| type == FileDialogType.Save)
9729 				{
9730 					getSaveFileName(&onOK, member, filters, null);
9731 				} else
9732 					getOpenFileName(&onOK, member, filters, null);
9733 			};
9734 		} else {
9735 			struct S {
9736 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
9737 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
9738 				} else mixin(q{
9739 				static foreach(idx, ignore; Params) {
9740 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
9741 				}
9742 				});
9743 			}
9744 			return () {
9745 				dialog((S s) {
9746 					try {
9747 						static if(is(typeof(t) Ret == return)) {
9748 							static if(is(Ret == void)) {
9749 								t(s.tupleof);
9750 							} else {
9751 								auto ret = t(s.tupleof);
9752 								import std.conv;
9753 								messageBox(to!string(ret), "Returned Value");
9754 							}
9755 						}
9756 					} catch(Exception e)
9757 						autoExceptionHandler(e);
9758 				}, null, __traits(identifier, fn));
9759 			};
9760 		}
9761 	}
9762 }
9763 
9764 private template hasAnyRelevantAnnotations(a...) {
9765 	bool helper() {
9766 		bool any;
9767 		foreach(attr; a) {
9768 			static if(is(typeof(attr) == .menu))
9769 				any = true;
9770 			else static if(is(typeof(attr) == .toolbar))
9771 				any = true;
9772 			else static if(is(attr == .separator))
9773 				any = true;
9774 			else static if(is(typeof(attr) == .accelerator))
9775 				any = true;
9776 			else static if(is(typeof(attr) == .hotkey))
9777 				any = true;
9778 			else static if(is(typeof(attr) == .icon))
9779 				any = true;
9780 			else static if(is(typeof(attr) == .label))
9781 				any = true;
9782 			else static if(is(typeof(attr) == .tip))
9783 				any = true;
9784 		}
9785 		return any;
9786 	}
9787 
9788 	enum bool hasAnyRelevantAnnotations = helper();
9789 }
9790 
9791 /++
9792 	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.
9793 +/
9794 class MainWindow : Window {
9795 	///
9796 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
9797 		super(initialWidth, initialHeight, title);
9798 
9799 		_clientArea = new ClientAreaWidget();
9800 		_clientArea.x = 0;
9801 		_clientArea.y = 0;
9802 		_clientArea.width = this.width;
9803 		_clientArea.height = this.height;
9804 		_clientArea.tabStop = false;
9805 
9806 		super.addChild(_clientArea);
9807 
9808 		statusBar = new StatusBar(this);
9809 	}
9810 
9811 	/++
9812 		Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though).
9813 
9814 	---
9815         struct Commands {
9816                 @menu("File") {
9817 			@toolbar("") // adds it to a generic toolbar
9818                         void New() {}
9819                         void Open() {}
9820                         void Save() {}
9821                         @separator
9822                         void Exit() @accelerator("Alt+F4") @hotkey('x') {
9823                                 window.close();
9824                         }
9825                 }
9826 
9827                 @menu("Edit") {
9828 			@icon(GenericIcons.Undo)
9829                         void Undo() {
9830                                 undo();
9831                         }
9832                         @separator
9833                         void Cut() {}
9834                         void Copy() {}
9835                         void Paste() {}
9836                 }
9837 
9838                 @menu("Help") {
9839                         void About() {}
9840                 }
9841         }
9842 
9843         Commands commands;
9844 
9845         window.setMenuAndToolbarFromAnnotatedCode(commands);
9846 	---
9847 
9848 	Note that you can call this function multiple times and it will add the items in order to the given items.
9849 
9850 	+/
9851 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
9852 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9853 	}
9854 	/// ditto
9855 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
9856 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9857 	}
9858 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
9859 		Action[] toolbarActions;
9860 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
9861 		Menu[string] mcs;
9862 
9863 		foreach(menu; menuBar.subMenus) {
9864 			mcs[menu.label] = menu;
9865 		}
9866 
9867 		foreach(memberName; __traits(derivedMembers, T)) {
9868 			static if(memberName != "this")
9869 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
9870 				.menu menu;
9871 				.toolbar toolbar;
9872 				bool separator;
9873 				.accelerator accelerator;
9874 				.hotkey hotkey;
9875 				.icon icon;
9876 				string label;
9877 				string tip;
9878 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
9879 					static if(is(typeof(attr) == .menu))
9880 						menu = attr;
9881 					else static if(is(typeof(attr) == .toolbar))
9882 						toolbar = attr;
9883 					else static if(is(attr == .separator))
9884 						separator = true;
9885 					else static if(is(typeof(attr) == .accelerator))
9886 						accelerator = attr;
9887 					else static if(is(typeof(attr) == .hotkey))
9888 						hotkey = attr;
9889 					else static if(is(typeof(attr) == .icon))
9890 						icon = attr;
9891 					else static if(is(typeof(attr) == .label))
9892 						label = attr.label;
9893 					else static if(is(typeof(attr) == .tip))
9894 						tip = attr.tip;
9895 				}
9896 
9897 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
9898 					ushort correctIcon = icon.id; // FIXME
9899 					if(label.length == 0)
9900 						label = memberName.toMenuLabel;
9901 
9902 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName));
9903 
9904 					auto action = new Action(label, correctIcon, handler);
9905 
9906 					if(accelerator.keyString.length) {
9907 						auto ke = KeyEvent.parse(accelerator.keyString);
9908 						action.accelerator = ke;
9909 						accelerators[ke.toStr] = handler;
9910 					}
9911 
9912 					if(toolbar !is .toolbar.init)
9913 						toolbarActions ~= action;
9914 					if(menu !is .menu.init) {
9915 						Menu mc;
9916 						if(menu.name in mcs) {
9917 							mc = mcs[menu.name];
9918 						} else {
9919 							mc = new Menu(menu.name, this);
9920 							menuBar.addItem(mc);
9921 							mcs[menu.name] = mc;
9922 						}
9923 
9924 						if(separator)
9925 							mc.addSeparator();
9926 						mc.addItem(new MenuItem(action));
9927 					}
9928 				}
9929 			}
9930 		}
9931 
9932 		this.menuBar = menuBar;
9933 
9934 		if(toolbarActions.length) {
9935 			auto tb = new ToolBar(toolbarActions, this);
9936 		}
9937 	}
9938 
9939 	void delegate()[string] accelerators;
9940 
9941 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9942 		auto str = event.originalKeyEvent.toStr;
9943 		if(auto acl = str in accelerators)
9944 			(*acl)();
9945 		super.defaultEventHandler_keydown(event);
9946 	}
9947 
9948 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
9949 		super.defaultEventHandler_mouseover(event);
9950 		if(this.statusBar !is null && event.target.statusTip.length)
9951 			this.statusBar.parts[0].content = event.target.statusTip;
9952 		else if(this.statusBar !is null && this.statusTip.length)
9953 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
9954 	}
9955 
9956 	override void addChild(Widget c, int position = int.max) {
9957 		if(auto tb = cast(ToolBar) c)
9958 			version(win32_widgets)
9959 				super.addChild(c, 0);
9960 			else version(custom_widgets)
9961 				super.addChild(c, menuBar ? 1 : 0);
9962 			else static assert(0);
9963 		else
9964 			clientArea.addChild(c, position);
9965 	}
9966 
9967 	ToolBar _toolBar;
9968 	///
9969 	ToolBar toolBar() { return _toolBar; }
9970 	///
9971 	ToolBar toolBar(ToolBar t) {
9972 		_toolBar = t;
9973 		foreach(child; this.children)
9974 			if(child is t)
9975 				return t;
9976 		version(win32_widgets)
9977 			super.addChild(t, 0);
9978 		else version(custom_widgets)
9979 			super.addChild(t, menuBar ? 1 : 0);
9980 		else static assert(0);
9981 		return t;
9982 	}
9983 
9984 	MenuBar _menu;
9985 	///
9986 	MenuBar menuBar() { return _menu; }
9987 	///
9988 	MenuBar menuBar(MenuBar m) {
9989 		if(m is _menu) {
9990 			version(custom_widgets)
9991 				queueRecomputeChildLayout();
9992 			return m;
9993 		}
9994 
9995 		if(_menu !is null) {
9996 			// make sure it is sanely removed
9997 			// FIXME
9998 		}
9999 
10000 		_menu = m;
10001 
10002 		version(win32_widgets) {
10003 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
10004 		} else version(custom_widgets) {
10005 			super.addChild(m, 0);
10006 
10007 		//	clientArea.y = menu.height;
10008 		//	clientArea.height = this.height - menu.height;
10009 
10010 			queueRecomputeChildLayout();
10011 		} else static assert(false);
10012 
10013 		return _menu;
10014 	}
10015 	private Widget _clientArea;
10016 	///
10017 	@property Widget clientArea() { return _clientArea; }
10018 	protected @property void clientArea(Widget wid) {
10019 		_clientArea = wid;
10020 	}
10021 
10022 	private StatusBar _statusBar;
10023 	/++
10024 		Returns the window's [StatusBar]. Be warned it may be `null`.
10025 	+/
10026 	@property StatusBar statusBar() { return _statusBar; }
10027 	/// ditto
10028 	@property void statusBar(StatusBar bar) {
10029 		if(_statusBar !is null)
10030 			_statusBar.removeWidget();
10031 		_statusBar = bar;
10032 		if(bar !is null)
10033 			super.addChild(_statusBar);
10034 	}
10035 }
10036 
10037 /+
10038 	This is really an implementation detail of [MainWindow]
10039 +/
10040 private class ClientAreaWidget : Widget {
10041 	this() {
10042 		this.tabStop = false;
10043 		super(null);
10044 		//sa = new ScrollableWidget(this);
10045 	}
10046 	/*
10047 	ScrollableWidget sa;
10048 	override void addChild(Widget w, int position) {
10049 		if(sa is null)
10050 			super.addChild(w, position);
10051 		else {
10052 			sa.addChild(w, position);
10053 			sa.setContentSize(this.minWidth + 1, this.minHeight);
10054 			writeln(sa.contentWidth, "x", sa.contentHeight);
10055 		}
10056 	}
10057 	*/
10058 }
10059 
10060 /**
10061 	Toolbars are lists of buttons (typically icons) that appear under the menu.
10062 	Each button ought to correspond to a menu item, represented by [Action] objects.
10063 */
10064 class ToolBar : Widget {
10065 	version(win32_widgets) {
10066 		private int idealHeight;
10067 		override int minHeight() { return idealHeight; }
10068 		override int maxHeight() { return idealHeight; }
10069 	} else version(custom_widgets) {
10070 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
10071 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
10072 	} else static assert(false);
10073 	override int heightStretchiness() { return 0; }
10074 
10075 	version(win32_widgets) {
10076 		HIMAGELIST imageListSmall;
10077 		HIMAGELIST imageListLarge;
10078 	}
10079 
10080 	this(Widget parent) {
10081 		this(null, parent);
10082 	}
10083 
10084 	version(win32_widgets)
10085 	void changeIconSize(bool useLarge) {
10086 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
10087 
10088 		/+
10089 		SIZE size;
10090 		import core.sys.windows.commctrl;
10091 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
10092 		idealHeight = size.cy + 4; // the plus 4 is a hack
10093 		+/
10094 
10095 		idealHeight = useLarge ? 34 : 26;
10096 
10097 		if(parent) {
10098 			parent.queueRecomputeChildLayout();
10099 			parent.redraw();
10100 		}
10101 
10102 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
10103 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
10104 	}
10105 
10106 	///
10107 	this(Action[] actions, Widget parent) {
10108 		super(parent);
10109 
10110 		tabStop = false;
10111 
10112 		version(win32_widgets) {
10113 			// so i like how the flat thing looks on windows, but not on wine
10114 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
10115 			// leave it commented
10116 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
10117 
10118 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
10119 
10120 			imageListSmall = ImageList_Create(
10121 				// width, height
10122 				16, 16,
10123 				ILC_COLOR16 | ILC_MASK,
10124 				16 /*numberOfButtons*/, 0);
10125 
10126 			imageListLarge = ImageList_Create(
10127 				// width, height
10128 				24, 24,
10129 				ILC_COLOR16 | ILC_MASK,
10130 				16 /*numberOfButtons*/, 0);
10131 
10132 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
10133 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
10134 
10135 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
10136 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
10137 
10138 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
10139 
10140 			TBBUTTON[] buttons;
10141 
10142 			// FIXME: I_IMAGENONE is if here is no icon
10143 			foreach(action; actions)
10144 				buttons ~= TBBUTTON(
10145 					MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
10146 					action.id,
10147 					TBSTATE_ENABLED, // state
10148 					0, // style
10149 					0, // reserved array, just zero it out
10150 					0, // dwData
10151 					cast(size_t) toWstringzInternal(action.label) // INT_PTR
10152 				);
10153 
10154 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
10155 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
10156 
10157 			/*
10158 			RECT rect;
10159 			GetWindowRect(hwnd, &rect);
10160 			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
10161 			*/
10162 
10163 			dpiChanged(); // to load the things calling changeIconSize the first time
10164 
10165 			assert(idealHeight);
10166 		} else version(custom_widgets) {
10167 			foreach(action; actions)
10168 				new ToolButton(action, this);
10169 		} else static assert(false);
10170 	}
10171 
10172 	override void recomputeChildLayout() {
10173 		.recomputeChildLayout!"width"(this);
10174 	}
10175 
10176 
10177 	version(win32_widgets)
10178 	override protected void dpiChanged() {
10179 		auto sz = scaleWithDpi(16);
10180 		if(sz >= 20)
10181 			changeIconSize(true);
10182 		else
10183 			changeIconSize(false);
10184 	}
10185 }
10186 
10187 enum toolbarIconSize = 24;
10188 
10189 /// 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.
10190 class ToolButton : Button {
10191 	///
10192 	this(string label, Widget parent) {
10193 		super(label, parent);
10194 		tabStop = false;
10195 	}
10196 	///
10197 	this(Action action, Widget parent) {
10198 		super(action.label, parent);
10199 		tabStop = false;
10200 		this.action = action;
10201 	}
10202 
10203 	version(custom_widgets)
10204 	override void defaultEventHandler_click(ClickEvent event) {
10205 		foreach(handler; action.triggered)
10206 			handler();
10207 	}
10208 
10209 	Action action;
10210 
10211 	override int maxWidth() { return toolbarIconSize; }
10212 	override int minWidth() { return toolbarIconSize; }
10213 	override int maxHeight() { return toolbarIconSize; }
10214 	override int minHeight() { return toolbarIconSize; }
10215 
10216 	version(custom_widgets)
10217 	override void paint(WidgetPainter painter) {
10218 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
10219 		painter.outlineColor = Color.black;
10220 
10221 		// I want to get from 16 to 24. that's * 3 / 2
10222 		static assert(toolbarIconSize >= 16);
10223 		enum multiplier = toolbarIconSize / 8;
10224 		enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0);
10225 		switch(action.iconId) {
10226 			case GenericIcons.New:
10227 				painter.fillColor = Color.white;
10228 				painter.drawPolygon(
10229 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor,
10230 					Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor,
10231 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor
10232 				);
10233 			break;
10234 			case GenericIcons.Save:
10235 				painter.fillColor = Color.white;
10236 				painter.outlineColor = Color.black;
10237 				painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10238 
10239 				// the label
10240 				painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor);
10241 
10242 				// the slider
10243 				painter.fillColor = Color.black;
10244 				painter.outlineColor = Color.black;
10245 				painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor);
10246 
10247 				painter.fillColor = Color.white;
10248 				painter.outlineColor = Color.white;
10249 				// the disc window
10250 				painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor);
10251 			break;
10252 			case GenericIcons.Open:
10253 				painter.fillColor = Color.white;
10254 				painter.drawPolygon(
10255 					Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor,
10256 					Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor);
10257 				painter.drawPolygon(
10258 					Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor,
10259 					Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor,
10260 					Point(2, 6) * multiplier / divisor);
10261 				//painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor);
10262 			break;
10263 			case GenericIcons.Copy:
10264 				painter.fillColor = Color.white;
10265 				painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor);
10266 				painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor);
10267 			break;
10268 			case GenericIcons.Cut:
10269 				painter.fillColor = Color.transparent;
10270 				painter.outlineColor = getComputedStyle.foregroundColor();
10271 				painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor);
10272 				painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor);
10273 				painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor);
10274 				painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor);
10275 			break;
10276 			case GenericIcons.Paste:
10277 				painter.fillColor = Color.white;
10278 				painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor);
10279 				painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10280 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor);
10281 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor);
10282 				painter.fillColor = Color.black;
10283 				painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor);
10284 			break;
10285 			case GenericIcons.Help:
10286 				painter.outlineColor = getComputedStyle.foregroundColor();
10287 				painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10288 			break;
10289 			case GenericIcons.Undo:
10290 				painter.fillColor = Color.transparent;
10291 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10292 				painter.outlineColor = Color.black;
10293 				painter.fillColor = Color.black;
10294 				painter.drawPolygon(
10295 					Point(4, 4) * multiplier / divisor,
10296 					Point(8, 2) * multiplier / divisor,
10297 					Point(8, 6) * multiplier / divisor,
10298 					Point(4, 4) * multiplier / divisor,
10299 				);
10300 			break;
10301 			case GenericIcons.Redo:
10302 				painter.fillColor = Color.transparent;
10303 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10304 				painter.outlineColor = Color.black;
10305 				painter.fillColor = Color.black;
10306 				painter.drawPolygon(
10307 					Point(10, 4) * multiplier / divisor,
10308 					Point(6, 2) * multiplier / divisor,
10309 					Point(6, 6) * multiplier / divisor,
10310 					Point(10, 4) * multiplier / divisor,
10311 				);
10312 			break;
10313 			default:
10314 				painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10315 		}
10316 		return bounds;
10317 		});
10318 	}
10319 
10320 }
10321 
10322 
10323 /++
10324 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
10325 +/
10326 class MenuBar : Widget {
10327 	MenuItem[] items;
10328 	Menu[] subMenus;
10329 
10330 	version(win32_widgets) {
10331 		HMENU handle;
10332 		///
10333 		this(Widget parent = null) {
10334 			super(parent);
10335 
10336 			handle = CreateMenu();
10337 			tabStop = false;
10338 		}
10339 	} else version(custom_widgets) {
10340 		///
10341 		this(Widget parent = null) {
10342 			tabStop = false; // these are selected some other way
10343 			super(parent);
10344 		}
10345 
10346 		mixin Padding!q{2};
10347 	} else static assert(false);
10348 
10349 	version(custom_widgets)
10350 	override void paint(WidgetPainter painter) {
10351 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
10352 	}
10353 
10354 	///
10355 	MenuItem addItem(MenuItem item) {
10356 		this.addChild(item);
10357 		items ~= item;
10358 		version(win32_widgets) {
10359 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
10360 		}
10361 		return item;
10362 	}
10363 
10364 
10365 	///
10366 	Menu addItem(Menu item) {
10367 
10368 		subMenus ~= item;
10369 
10370 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
10371 
10372 		addChild(mbItem);
10373 		items ~= mbItem;
10374 
10375 		version(win32_widgets) {
10376 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
10377 		} else version(custom_widgets) {
10378 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
10379 				item.popup(mbItem);
10380 			};
10381 		} else static assert(false);
10382 
10383 		return item;
10384 	}
10385 
10386 	override void recomputeChildLayout() {
10387 		.recomputeChildLayout!"width"(this);
10388 	}
10389 
10390 	override int maxHeight() { return defaultLineHeight + 4; }
10391 	override int minHeight() { return defaultLineHeight + 4; }
10392 }
10393 
10394 
10395 /**
10396 	Status bars appear at the bottom of a MainWindow.
10397 	They are made out of Parts, with a width and content.
10398 
10399 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
10400 
10401 
10402 	sb.parts[0].content = "Status bar text!";
10403 */
10404 class StatusBar : Widget {
10405 	private Part[] partsArray;
10406 	///
10407 	struct Parts {
10408 		@disable this();
10409 		this(StatusBar owner) { this.owner = owner; }
10410 		//@disable this(this);
10411 		///
10412 		@property int length() { return cast(int) owner.partsArray.length; }
10413 		private StatusBar owner;
10414 		private this(StatusBar owner, Part[] parts) {
10415 			this.owner.partsArray = parts;
10416 			this.owner = owner;
10417 		}
10418 		///
10419 		Part opIndex(int p) {
10420 			if(owner.partsArray.length == 0)
10421 				this ~= new StatusBar.Part(0);
10422 			return owner.partsArray[p];
10423 		}
10424 
10425 		///
10426 		Part opOpAssign(string op : "~" )(Part p) {
10427 			assert(owner.partsArray.length < 255);
10428 			p.owner = this.owner;
10429 			p.idx = cast(int) owner.partsArray.length;
10430 			owner.partsArray ~= p;
10431 
10432 			owner.queueRecomputeChildLayout();
10433 
10434 			version(win32_widgets) {
10435 				int[256] pos;
10436 				int cpos;
10437 				foreach(idx, part; owner.partsArray) {
10438 					if(idx + 1 == owner.partsArray.length)
10439 						pos[idx] = -1;
10440 					else {
10441 						cpos += part.currentlyAssignedWidth;
10442 						pos[idx] = cpos;
10443 					}
10444 				}
10445 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
10446 			} else version(custom_widgets) {
10447 				owner.redraw();
10448 			} else static assert(false);
10449 
10450 			return p;
10451 		}
10452 	}
10453 
10454 	private Parts _parts;
10455 	///
10456 	final @property Parts parts() {
10457 		return _parts;
10458 	}
10459 
10460 	/++
10461 
10462 	+/
10463 	static class Part {
10464 		/++
10465 			History:
10466 				Added September 1, 2023 (dub v11.1)
10467 		+/
10468 		enum WidthUnits {
10469 			/++
10470 				Unscaled pixels as they appear on screen.
10471 
10472 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
10473 			+/
10474 			DeviceDependentPixels,
10475 			/++
10476 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
10477 			+/
10478 			DeviceIndependentPixels,
10479 			/++
10480 				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`).
10481 			+/
10482 			ApproximateCharacters,
10483 			/++
10484 				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.
10485 
10486 				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.
10487 			+/
10488 			Proportional
10489 		}
10490 		private WidthUnits units;
10491 		private int width;
10492 		private StatusBar owner;
10493 
10494 		private int currentlyAssignedWidth;
10495 
10496 		/++
10497 			History:
10498 				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.
10499 
10500 				It now allows you to provide your own value for [WidthUnits].
10501 
10502 				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`.
10503 		+/
10504 		this(int w, WidthUnits units = WidthUnits.Proportional) {
10505 			this.units = units;
10506 			this.width = w;
10507 		}
10508 
10509 		/// ditto
10510 		this(int w = 0) {
10511 			if(w == 0)
10512 				this(w, WidthUnits.Proportional);
10513 			else
10514 				this(w, WidthUnits.DeviceDependentPixels);
10515 		}
10516 
10517 		private int idx;
10518 		private string _content;
10519 		///
10520 		@property string content() { return _content; }
10521 		///
10522 		@property void content(string s) {
10523 			version(win32_widgets) {
10524 				_content = s;
10525 				WCharzBuffer bfr = WCharzBuffer(s);
10526 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
10527 			} else version(custom_widgets) {
10528 				if(_content != s) {
10529 					_content = s;
10530 					owner.redraw();
10531 				}
10532 			} else static assert(false);
10533 		}
10534 	}
10535 	string simpleModeContent;
10536 	bool inSimpleMode;
10537 
10538 
10539 	///
10540 	this(Widget parent) {
10541 		super(null); // FIXME
10542 		_parts = Parts(this);
10543 		tabStop = false;
10544 		version(win32_widgets) {
10545 			parentWindow = parent.parentWindow;
10546 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
10547 
10548 			RECT rect;
10549 			GetWindowRect(hwnd, &rect);
10550 			idealHeight = rect.bottom - rect.top;
10551 			assert(idealHeight);
10552 		} else version(custom_widgets) {
10553 		} else static assert(false);
10554 	}
10555 
10556 	override void recomputeChildLayout() {
10557 		int remainingLength = this.width;
10558 
10559 		int proportionalSum;
10560 		int proportionalCount;
10561 		foreach(idx, part; this.partsArray) {
10562 			with(Part.WidthUnits)
10563 			final switch(part.units) {
10564 				case DeviceDependentPixels:
10565 					part.currentlyAssignedWidth = part.width;
10566 					remainingLength -= part.currentlyAssignedWidth;
10567 				break;
10568 				case DeviceIndependentPixels:
10569 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
10570 					remainingLength -= part.currentlyAssignedWidth;
10571 				break;
10572 				case ApproximateCharacters:
10573 					auto cs = getComputedStyle();
10574 					auto font = cs.font;
10575 
10576 					part.currentlyAssignedWidth = font.averageWidth * this.width;
10577 					remainingLength -= part.currentlyAssignedWidth;
10578 				break;
10579 				case Proportional:
10580 					proportionalSum += part.width;
10581 					proportionalCount ++;
10582 				break;
10583 			}
10584 		}
10585 
10586 		foreach(part; this.partsArray) {
10587 			if(part.units == Part.WidthUnits.Proportional) {
10588 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
10589 				if(proportion == 0)
10590 					proportion = 1;
10591 
10592 				if(proportionalSum == 0)
10593 					proportionalSum = proportionalCount;
10594 
10595 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
10596 			}
10597 		}
10598 
10599 		super.recomputeChildLayout();
10600 	}
10601 
10602 	version(win32_widgets)
10603 	override protected void dpiChanged() {
10604 		RECT rect;
10605 		GetWindowRect(hwnd, &rect);
10606 		idealHeight = rect.bottom - rect.top;
10607 		assert(idealHeight);
10608 	}
10609 
10610 	version(custom_widgets)
10611 	override void paint(WidgetPainter painter) {
10612 		auto cs = getComputedStyle();
10613 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10614 		int cpos = 0;
10615 		foreach(idx, part; this.partsArray) {
10616 			auto partWidth = part.currentlyAssignedWidth;
10617 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
10618 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
10619 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
10620 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
10621 
10622 			painter.outlineColor = cs.foregroundColor();
10623 			painter.fillColor = cs.foregroundColor();
10624 
10625 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
10626 			cpos += partWidth;
10627 		}
10628 	}
10629 
10630 
10631 	version(win32_widgets) {
10632 		private int idealHeight;
10633 		override int maxHeight() { return idealHeight; }
10634 		override int minHeight() { return idealHeight; }
10635 	} else version(custom_widgets) {
10636 		override int maxHeight() { return defaultLineHeight + 4; }
10637 		override int minHeight() { return defaultLineHeight + 4; }
10638 	} else static assert(false);
10639 }
10640 
10641 /// Displays an in-progress indicator without known values
10642 version(none)
10643 class IndefiniteProgressBar : Widget {
10644 	version(win32_widgets)
10645 	this(Widget parent) {
10646 		super(parent);
10647 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
10648 		tabStop = false;
10649 	}
10650 	override int minHeight() { return 10; }
10651 }
10652 
10653 /// A progress bar with a known endpoint and completion amount
10654 class ProgressBar : Widget {
10655 	/++
10656 		History:
10657 			Added March 16, 2022 (dub v10.7)
10658 	+/
10659 	this(int min, int max, Widget parent) {
10660 		this(parent);
10661 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
10662 	}
10663 	this(Widget parent) {
10664 		version(win32_widgets) {
10665 			super(parent);
10666 			createWin32Window(this, "msctls_progress32"w, "", 0);
10667 			tabStop = false;
10668 		} else version(custom_widgets) {
10669 			super(parent);
10670 			max = 100;
10671 			step = 10;
10672 			tabStop = false;
10673 		} else static assert(0);
10674 	}
10675 
10676 	version(custom_widgets)
10677 	override void paint(WidgetPainter painter) {
10678 		auto cs = getComputedStyle();
10679 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10680 		painter.fillColor = cs.progressBarColor;
10681 		painter.drawRectangle(Point(0, 0), width * current / max, height);
10682 	}
10683 
10684 
10685 	version(custom_widgets) {
10686 		int current;
10687 		int max;
10688 		int step;
10689 	}
10690 
10691 	///
10692 	void advanceOneStep() {
10693 		version(win32_widgets)
10694 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
10695 		else version(custom_widgets)
10696 			addToPosition(step);
10697 		else static assert(false);
10698 	}
10699 
10700 	///
10701 	void setStepIncrement(int increment) {
10702 		version(win32_widgets)
10703 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
10704 		else version(custom_widgets)
10705 			step = increment;
10706 		else static assert(false);
10707 	}
10708 
10709 	///
10710 	void addToPosition(int amount) {
10711 		version(win32_widgets)
10712 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
10713 		else version(custom_widgets)
10714 			setPosition(current + amount);
10715 		else static assert(false);
10716 	}
10717 
10718 	///
10719 	void setPosition(int pos) {
10720 		version(win32_widgets)
10721 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
10722 		else version(custom_widgets) {
10723 			current = pos;
10724 			if(current > max)
10725 				current = max;
10726 			redraw();
10727 		}
10728 		else static assert(false);
10729 	}
10730 
10731 	///
10732 	void setRange(ushort min, ushort max) {
10733 		version(win32_widgets)
10734 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
10735 		else version(custom_widgets) {
10736 			this.max = max;
10737 		}
10738 		else static assert(false);
10739 	}
10740 
10741 	override int minHeight() { return 10; }
10742 }
10743 
10744 version(custom_widgets)
10745 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
10746 	thisLabel.reserve(label.length);
10747 	bool justSawAmpersand;
10748 	foreach(ch; label) {
10749 		if(justSawAmpersand) {
10750 			justSawAmpersand = false;
10751 			if(ch == '&') {
10752 				goto plain;
10753 			}
10754 			thisAccelerator = ch;
10755 		} else {
10756 			if(ch == '&') {
10757 				justSawAmpersand = true;
10758 				continue;
10759 			}
10760 			plain:
10761 			thisLabel ~= ch;
10762 		}
10763 	}
10764 }
10765 
10766 /++
10767 	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.
10768 
10769 
10770 	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
10771 
10772 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
10773 
10774 	History:
10775 		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.
10776 +/
10777 class Fieldset : Widget {
10778 	// FIXME: on Windows,it doesn't draw the background on the label
10779 	// on X, it doesn't fix the clipping rectangle for it
10780 	version(win32_widgets)
10781 		override int paddingTop() { return defaultLineHeight; }
10782 	else version(custom_widgets)
10783 		override int paddingTop() { return defaultLineHeight + 2; }
10784 	else static assert(false);
10785 	override int paddingBottom() { return 6; }
10786 	override int paddingLeft() { return 6; }
10787 	override int paddingRight() { return 6; }
10788 
10789 	override int marginLeft() { return 6; }
10790 	override int marginRight() { return 6; }
10791 	override int marginTop() { return 2; }
10792 	override int marginBottom() { return 2; }
10793 
10794 	string legend;
10795 
10796 	version(custom_widgets) private dchar accelerator;
10797 
10798 	this(string legend, Widget parent) {
10799 		version(win32_widgets) {
10800 			super(parent);
10801 			this.legend = legend;
10802 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
10803 			tabStop = false;
10804 		} else version(custom_widgets) {
10805 			super(parent);
10806 			tabStop = false;
10807 
10808 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
10809 		} else static assert(0);
10810 	}
10811 
10812 	version(custom_widgets)
10813 	override void paint(WidgetPainter painter) {
10814 		auto dlh = defaultLineHeight;
10815 
10816 		painter.fillColor = Color.transparent;
10817 		auto cs = getComputedStyle();
10818 		painter.pen = Pen(cs.foregroundColor, 1);
10819 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
10820 
10821 		auto tx = painter.textSize(legend);
10822 		painter.outlineColor = Color.transparent;
10823 
10824 		version(Windows) {
10825 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
10826 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
10827 			SelectObject(painter.impl.hdc, b);
10828 		} else static if(UsingSimpledisplayX11) {
10829 			painter.fillColor = getComputedStyle().windowBackgroundColor;
10830 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
10831 		}
10832 		painter.outlineColor = cs.foregroundColor;
10833 		painter.drawText(Point(8, 0), legend);
10834 	}
10835 
10836 	override int maxHeight() {
10837 		auto m = paddingTop() + paddingBottom();
10838 		foreach(child; children) {
10839 			auto mh = child.maxHeight();
10840 			if(mh == int.max)
10841 				return int.max;
10842 			m += mh;
10843 			m += child.marginBottom();
10844 			m += child.marginTop();
10845 		}
10846 		m += 6;
10847 		if(m < minHeight)
10848 			return minHeight;
10849 		return m;
10850 	}
10851 
10852 	override int minHeight() {
10853 		auto m = paddingTop() + paddingBottom();
10854 		foreach(child; children) {
10855 			m += child.minHeight();
10856 			m += child.marginBottom();
10857 			m += child.marginTop();
10858 		}
10859 		return m + 6;
10860 	}
10861 
10862 	override int minWidth() {
10863 		return 6 + cast(int) this.legend.length * 7;
10864 	}
10865 }
10866 
10867 /++
10868 	$(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")
10869 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
10870 +/
10871 version(minigui_screenshots)
10872 @Screenshot("Fieldset")
10873 unittest {
10874 	auto window = new Window(200, 100);
10875 	auto set = new Fieldset("Baby will", window);
10876 	auto option1 = new Radiobox("Eat", set);
10877 	auto option2 = new Radiobox("Cry", set);
10878 	auto option3 = new Radiobox("Sleep", set);
10879 	window.loop();
10880 }
10881 
10882 /// Draws a line
10883 class HorizontalRule : Widget {
10884 	mixin Margin!q{ 2 };
10885 	override int minHeight() { return 2; }
10886 	override int maxHeight() { return 2; }
10887 
10888 	///
10889 	this(Widget parent) {
10890 		super(parent);
10891 	}
10892 
10893 	override void paint(WidgetPainter painter) {
10894 		auto cs = getComputedStyle();
10895 		painter.outlineColor = cs.darkAccentColor;
10896 		painter.drawLine(Point(0, 0), Point(width, 0));
10897 		painter.outlineColor = cs.lightAccentColor;
10898 		painter.drawLine(Point(0, 1), Point(width, 1));
10899 	}
10900 }
10901 
10902 version(minigui_screenshots)
10903 @Screenshot("HorizontalRule")
10904 /++
10905 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
10906 
10907 +/
10908 unittest {
10909 	auto window = new Window(200, 100);
10910 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
10911 	new HorizontalRule(window);
10912 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
10913 	window.loop();
10914 }
10915 
10916 /// ditto
10917 class VerticalRule : Widget {
10918 	mixin Margin!q{ 2 };
10919 	override int minWidth() { return 2; }
10920 	override int maxWidth() { return 2; }
10921 
10922 	///
10923 	this(Widget parent) {
10924 		super(parent);
10925 	}
10926 
10927 	override void paint(WidgetPainter painter) {
10928 		auto cs = getComputedStyle();
10929 		painter.outlineColor = cs.darkAccentColor;
10930 		painter.drawLine(Point(0, 0), Point(0, height));
10931 		painter.outlineColor = cs.lightAccentColor;
10932 		painter.drawLine(Point(1, 0), Point(1, height));
10933 	}
10934 }
10935 
10936 
10937 ///
10938 class Menu : Window {
10939 	void remove() {
10940 		foreach(i, child; parentWindow.children)
10941 			if(child is this) {
10942 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
10943 				break;
10944 			}
10945 		parentWindow.redraw();
10946 
10947 		parentWindow.releaseMouseCapture();
10948 	}
10949 
10950 	///
10951 	void addSeparator() {
10952 		version(win32_widgets)
10953 			AppendMenu(handle, MF_SEPARATOR, 0, null);
10954 		else version(custom_widgets)
10955 			auto hr = new HorizontalRule(this);
10956 		else static assert(0);
10957 	}
10958 
10959 	override int paddingTop() { return 4; }
10960 	override int paddingBottom() { return 4; }
10961 	override int paddingLeft() { return 2; }
10962 	override int paddingRight() { return 2; }
10963 
10964 	version(win32_widgets) {}
10965 	else version(custom_widgets) {
10966 		SimpleWindow dropDown;
10967 		Widget menuParent;
10968 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
10969 			this.menuParent = parent;
10970 
10971 			int w = 150;
10972 			int h = paddingTop + paddingBottom;
10973 			if(this.children.length) {
10974 				// hacking it to get the ideal height out of recomputeChildLayout
10975 				this.width = w;
10976 				this.height = h;
10977 				this.recomputeChildLayoutEntry();
10978 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
10979 				h += paddingBottom;
10980 
10981 				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
10982 			}
10983 
10984 			if(offsetY == int.min)
10985 				offsetY = parent.defaultLineHeight;
10986 
10987 			auto coord = parent.globalCoordinates();
10988 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
10989 			this.x = 0;
10990 			this.y = 0;
10991 			this.width = dropDown.width;
10992 			this.height = dropDown.height;
10993 			this.drawableWindow = dropDown;
10994 			this.recomputeChildLayoutEntry();
10995 
10996 			static if(UsingSimpledisplayX11)
10997 				XSync(XDisplayConnection.get, 0);
10998 
10999 			dropDown.visibilityChanged = (bool visible) {
11000 				if(visible) {
11001 					this.redraw();
11002 					dropDown.grabInput();
11003 				} else {
11004 					dropDown.releaseInputGrab();
11005 				}
11006 			};
11007 
11008 			dropDown.show();
11009 
11010 			clickListener = this.addEventListener((scope ClickEvent ev) {
11011 				unpopup();
11012 				// need to unlock asap just in case other user handlers block...
11013 				static if(UsingSimpledisplayX11)
11014 					flushGui();
11015 			}, true /* again for asap action */);
11016 		}
11017 
11018 		EventListener clickListener;
11019 	}
11020 	else static assert(false);
11021 
11022 	version(custom_widgets)
11023 	void unpopup() {
11024 		mouseLastOver = mouseLastDownOn = null;
11025 		dropDown.hide();
11026 		if(!menuParent.parentWindow.win.closed) {
11027 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
11028 				maw.setDynamicState(DynamicState.depressed, false);
11029 				maw.setDynamicState(DynamicState.hover, false);
11030 				maw.redraw();
11031 			}
11032 			// menuParent.parentWindow.win.focus();
11033 		}
11034 		clickListener.disconnect();
11035 	}
11036 
11037 	MenuItem[] items;
11038 
11039 	///
11040 	MenuItem addItem(MenuItem item) {
11041 		addChild(item);
11042 		items ~= item;
11043 		version(win32_widgets) {
11044 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11045 		}
11046 		return item;
11047 	}
11048 
11049 	string label;
11050 
11051 	version(win32_widgets) {
11052 		HMENU handle;
11053 		///
11054 		this(string label, Widget parent) {
11055 			// not actually passing the parent since it effs up the drawing
11056 			super(cast(Widget) null);// parent);
11057 			this.label = label;
11058 			handle = CreatePopupMenu();
11059 		}
11060 	} else version(custom_widgets) {
11061 		///
11062 		this(string label, Widget parent) {
11063 
11064 			if(dropDown) {
11065 				dropDown.close();
11066 			}
11067 			dropDown = new SimpleWindow(
11068 				150, 4,
11069 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
11070 
11071 			this.label = label;
11072 
11073 			super(dropDown);
11074 		}
11075 	} else static assert(false);
11076 
11077 	override int maxHeight() { return defaultLineHeight; }
11078 	override int minHeight() { return defaultLineHeight; }
11079 
11080 	version(custom_widgets)
11081 	override void paint(WidgetPainter painter) {
11082 		this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
11083 	}
11084 }
11085 
11086 /++
11087 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
11088 +/
11089 class MenuItem : MouseActivatedWidget {
11090 	Menu submenu;
11091 
11092 	Action action;
11093 	string label;
11094 
11095 	override int paddingLeft() { return 4; }
11096 
11097 	override int maxHeight() { return defaultLineHeight + 4; }
11098 	override int minHeight() { return defaultLineHeight + 4; }
11099 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
11100 	override int maxWidth() {
11101 		if(cast(MenuBar) parent) {
11102 			return minWidth();
11103 		}
11104 		return int.max;
11105 	}
11106 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
11107 	this(string lbl, Widget parent = null) {
11108 		super(parent);
11109 		//label = lbl; // FIXME
11110 		foreach(char ch; lbl) // FIXME
11111 			if(ch != '&') // FIXME
11112 				label ~= ch; // FIXME
11113 		tabStop = false; // these are selected some other way
11114 	}
11115 
11116 	///
11117 	this(Action action, Widget parent = null) {
11118 		assert(action !is null);
11119 		this(action.label, parent);
11120 		this.action = action;
11121 		tabStop = false; // these are selected some other way
11122 	}
11123 
11124 	version(custom_widgets)
11125 	override void paint(WidgetPainter painter) {
11126 		auto cs = getComputedStyle();
11127 		if(dynamicState & DynamicState.depressed)
11128 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11129 		if(dynamicState & DynamicState.hover)
11130 			painter.outlineColor = cs.activeMenuItemColor;
11131 		else
11132 			painter.outlineColor = cs.foregroundColor;
11133 		painter.fillColor = Color.transparent;
11134 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11135 		if(action && action.accelerator !is KeyEvent.init) {
11136 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
11137 
11138 		}
11139 	}
11140 
11141 	static class Style : Widget.Style {
11142 		override bool variesWithState(ulong dynamicStateFlags) {
11143 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11144 		}
11145 	}
11146 	mixin OverrideStyle!Style;
11147 
11148 	override void defaultEventHandler_triggered(Event event) {
11149 		if(action)
11150 		foreach(handler; action.triggered)
11151 			handler();
11152 
11153 		if(auto pmenu = cast(Menu) this.parent)
11154 			pmenu.remove();
11155 
11156 		super.defaultEventHandler_triggered(event);
11157 	}
11158 }
11159 
11160 version(win32_widgets)
11161 /// A "mouse activiated widget" is really just an abstract variant of button.
11162 class MouseActivatedWidget : Widget {
11163 	@property bool isChecked() {
11164 		assert(hwnd);
11165 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
11166 
11167 	}
11168 	@property void isChecked(bool state) {
11169 		assert(hwnd);
11170 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
11171 
11172 	}
11173 
11174 	override void handleWmCommand(ushort cmd, ushort id) {
11175 		if(cmd == 0) {
11176 			auto event = new Event(EventType.triggered, this);
11177 			event.dispatch();
11178 		}
11179 	}
11180 
11181 	this(Widget parent) {
11182 		super(parent);
11183 	}
11184 }
11185 else version(custom_widgets)
11186 /// ditto
11187 class MouseActivatedWidget : Widget {
11188 	@property bool isChecked() { return isChecked_; }
11189 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
11190 
11191 	private bool isChecked_;
11192 
11193 	this(Widget parent) {
11194 		super(parent);
11195 
11196 		addEventListener((MouseDownEvent ev) {
11197 			if(ev.button == MouseButton.left) {
11198 				setDynamicState(DynamicState.depressed, true);
11199 				setDynamicState(DynamicState.hover, true);
11200 				redraw();
11201 			}
11202 		});
11203 
11204 		addEventListener((MouseUpEvent ev) {
11205 			if(ev.button == MouseButton.left) {
11206 				setDynamicState(DynamicState.depressed, false);
11207 				setDynamicState(DynamicState.hover, false);
11208 				redraw();
11209 			}
11210 		});
11211 
11212 		addEventListener((MouseMoveEvent mme) {
11213 			if(!(mme.state & ModifierState.leftButtonDown)) {
11214 				if(dynamicState_ & DynamicState.depressed) {
11215 					setDynamicState(DynamicState.depressed, false);
11216 					redraw();
11217 				}
11218 			}
11219 		});
11220 	}
11221 
11222 	override void defaultEventHandler_focus(Event ev) {
11223 		super.defaultEventHandler_focus(ev);
11224 		this.redraw();
11225 	}
11226 	override void defaultEventHandler_blur(Event ev) {
11227 		super.defaultEventHandler_blur(ev);
11228 		setDynamicState(DynamicState.depressed, false);
11229 		this.redraw();
11230 	}
11231 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
11232 		super.defaultEventHandler_keydown(ev);
11233 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
11234 			setDynamicState(DynamicState.depressed, true);
11235 			setDynamicState(DynamicState.hover, true);
11236 			this.redraw();
11237 		}
11238 	}
11239 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
11240 		super.defaultEventHandler_keyup(ev);
11241 		if(!(dynamicState & DynamicState.depressed))
11242 			return;
11243 		setDynamicState(DynamicState.depressed, false);
11244 		setDynamicState(DynamicState.hover, false);
11245 		this.redraw();
11246 
11247 		auto event = new Event(EventType.triggered, this);
11248 		event.sendDirectly();
11249 	}
11250 	override void defaultEventHandler_click(ClickEvent ev) {
11251 		super.defaultEventHandler_click(ev);
11252 		if(ev.button == MouseButton.left) {
11253 			auto event = new Event(EventType.triggered, this);
11254 			event.sendDirectly();
11255 		}
11256 	}
11257 
11258 }
11259 else static assert(false);
11260 
11261 /*
11262 /++
11263 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
11264 
11265 	Basically the same as a checkbox.
11266 +/
11267 class OnOffSwitch : MouseActivatedWidget {
11268 
11269 }
11270 */
11271 
11272 /++
11273 	History:
11274 		Added June 15, 2021 (dub v10.1)
11275 +/
11276 struct ImageLabel {
11277 	/++
11278 		Defines a label+image combo used by some widgets.
11279 
11280 		If you provide just a text label, that is all the widget will try to
11281 		display. Or just an image will display just that. If you provide both,
11282 		it may display both text and image side by side or display the image
11283 		and offer text on an input event depending on the widget.
11284 
11285 		History:
11286 			The `alignment` parameter was added on September 27, 2021
11287 	+/
11288 	this(string label, TextAlignment alignment = TextAlignment.Center) {
11289 		this.label = label;
11290 		this.displayFlags = DisplayFlags.displayText;
11291 		this.alignment = alignment;
11292 	}
11293 
11294 	/// ditto
11295 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11296 		this.label = label;
11297 		this.image = image;
11298 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11299 		this.alignment = alignment;
11300 	}
11301 
11302 	/// ditto
11303 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11304 		this.image = image;
11305 		this.displayFlags = DisplayFlags.displayImage;
11306 		this.alignment = alignment;
11307 	}
11308 
11309 	/// ditto
11310 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
11311 		this.label = label;
11312 		this.image = image;
11313 		this.alignment = alignment;
11314 		this.displayFlags = displayFlags;
11315 	}
11316 
11317 	string label;
11318 	MemoryImage image;
11319 
11320 	enum DisplayFlags {
11321 		displayText = 1 << 0,
11322 		displayImage = 1 << 1,
11323 	}
11324 
11325 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11326 
11327 	TextAlignment alignment;
11328 }
11329 
11330 /++
11331 	A basic checked or not checked box with an attached label.
11332 
11333 
11334 	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
11335 
11336 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11337 
11338 	History:
11339 		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.
11340 +/
11341 class Checkbox : MouseActivatedWidget {
11342 	version(win32_widgets) {
11343 		override int maxHeight() { return scaleWithDpi(16); }
11344 		override int minHeight() { return scaleWithDpi(16); }
11345 	} else version(custom_widgets) {
11346 		private enum buttonSize = 16;
11347 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11348 		override int minHeight() { return maxHeight(); }
11349 	} else static assert(0);
11350 
11351 	override int marginLeft() { return 4; }
11352 
11353 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
11354 
11355 	/++
11356 		Just an alias because I keep typing checked out of web habit.
11357 
11358 		History:
11359 			Added May 31, 2021
11360 	+/
11361 	alias checked = isChecked;
11362 
11363 	private string label;
11364 	private dchar accelerator;
11365 
11366 	/++
11367 	+/
11368 	this(string label, Widget parent) {
11369 		this(ImageLabel(label), Appearance.checkbox, parent);
11370 	}
11371 
11372 	/// ditto
11373 	this(string label, Appearance appearance, Widget parent) {
11374 		this(ImageLabel(label), appearance, parent);
11375 	}
11376 
11377 	/++
11378 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
11379 
11380 		History:
11381 			Added June 29, 2021 (dub v10.2)
11382 	+/
11383 	enum Appearance {
11384 		checkbox, /// a normal checkbox
11385 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
11386 		//sliderswitch,
11387 	}
11388 	private Appearance appearance;
11389 
11390 	/// ditto
11391 	private this(ImageLabel label, Appearance appearance, Widget parent) {
11392 		super(parent);
11393 		version(win32_widgets) {
11394 			this.label = label.label;
11395 
11396 			uint extraStyle;
11397 			final switch(appearance) {
11398 				case Appearance.checkbox:
11399 				break;
11400 				case Appearance.pushbutton:
11401 					extraStyle |= BS_PUSHLIKE;
11402 				break;
11403 			}
11404 
11405 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
11406 		} else version(custom_widgets) {
11407 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
11408 		} else static assert(0);
11409 	}
11410 
11411 	version(custom_widgets)
11412 	override void paint(WidgetPainter painter) {
11413 		auto cs = getComputedStyle();
11414 		if(isFocused()) {
11415 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11416 			painter.fillColor = cs.windowBackgroundColor;
11417 			painter.drawRectangle(Point(0, 0), width, height);
11418 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11419 		} else {
11420 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
11421 			painter.fillColor = cs.windowBackgroundColor;
11422 			painter.drawRectangle(Point(0, 0), width, height);
11423 		}
11424 
11425 
11426 		painter.outlineColor = Color.black;
11427 		painter.fillColor = Color.white;
11428 		enum rectOffset = 2;
11429 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
11430 
11431 		if(isChecked) {
11432 			auto size = scaleWithDpi(2);
11433 			painter.pen = Pen(Color.black, size);
11434 			// I'm using height so the checkbox is square
11435 			enum padding = 3;
11436 			painter.drawLine(
11437 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
11438 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
11439 			);
11440 			painter.drawLine(
11441 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
11442 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
11443 			);
11444 
11445 			painter.pen = Pen(Color.black, 1);
11446 		}
11447 
11448 		if(label !is null) {
11449 			painter.outlineColor = cs.foregroundColor();
11450 			painter.fillColor = cs.foregroundColor();
11451 
11452 			// i want the centerline of the text to be aligned with the centerline of the checkbox
11453 			/+
11454 			auto font = cs.font();
11455 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
11456 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
11457 			+/
11458 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
11459 		}
11460 	}
11461 
11462 	override void defaultEventHandler_triggered(Event ev) {
11463 		isChecked = !isChecked;
11464 
11465 		this.emit!(ChangeEvent!bool)(&isChecked);
11466 
11467 		redraw();
11468 	}
11469 
11470 	/// Emits a change event with the checked state
11471 	mixin Emits!(ChangeEvent!bool);
11472 }
11473 
11474 /// Adds empty space to a layout.
11475 class VerticalSpacer : Widget {
11476 	///
11477 	this(Widget parent) {
11478 		super(parent);
11479 	}
11480 }
11481 
11482 /// ditto
11483 class HorizontalSpacer : Widget {
11484 	///
11485 	this(Widget parent) {
11486 		super(parent);
11487 		this.tabStop = false;
11488 	}
11489 }
11490 
11491 
11492 /++
11493 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
11494 
11495 
11496 	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
11497 
11498 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11499 
11500 	History:
11501 		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.
11502 +/
11503 class Radiobox : MouseActivatedWidget {
11504 
11505 	version(win32_widgets) {
11506 		override int maxHeight() { return scaleWithDpi(16); }
11507 		override int minHeight() { return scaleWithDpi(16); }
11508 	} else version(custom_widgets) {
11509 		private enum buttonSize = 16;
11510 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11511 		override int minHeight() { return maxHeight(); }
11512 	} else static assert(0);
11513 
11514 	override int marginLeft() { return 4; }
11515 
11516 	// FIXME: make a label getter
11517 	private string label;
11518 	private dchar accelerator;
11519 
11520 	/++
11521 
11522 	+/
11523 	this(string label, Widget parent) {
11524 		super(parent);
11525 		version(win32_widgets) {
11526 			this.label = label;
11527 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
11528 		} else version(custom_widgets) {
11529 			label.extractWindowsStyleLabel(this.label, this.accelerator);
11530 			height = 16;
11531 			width = height + 4 + cast(int) label.length * 16;
11532 		}
11533 	}
11534 
11535 	version(custom_widgets)
11536 	override void paint(WidgetPainter painter) {
11537 		auto cs = getComputedStyle();
11538 
11539 		if(isFocused) {
11540 			painter.fillColor = cs.windowBackgroundColor;
11541 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11542 		} else {
11543 			painter.fillColor = cs.windowBackgroundColor;
11544 			painter.outlineColor = cs.windowBackgroundColor;
11545 		}
11546 		painter.drawRectangle(Point(0, 0), width, height);
11547 
11548 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11549 
11550 		painter.outlineColor = Color.black;
11551 		painter.fillColor = Color.white;
11552 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
11553 		if(isChecked) {
11554 			painter.outlineColor = Color.black;
11555 			painter.fillColor = Color.black;
11556 			// I'm using height so the checkbox is square
11557 			auto size = scaleWithDpi(2);
11558 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
11559 		}
11560 
11561 		painter.outlineColor = cs.foregroundColor();
11562 		painter.fillColor = cs.foregroundColor();
11563 
11564 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11565 	}
11566 
11567 
11568 	override void defaultEventHandler_triggered(Event ev) {
11569 		isChecked = true;
11570 
11571 		if(this.parent) {
11572 			foreach(child; this.parent.children) {
11573 				if(child is this) continue;
11574 				if(auto rb = cast(Radiobox) child) {
11575 					rb.isChecked = false;
11576 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
11577 					rb.redraw();
11578 				}
11579 			}
11580 		}
11581 
11582 		this.emit!(ChangeEvent!bool)(&this.isChecked);
11583 
11584 		redraw();
11585 	}
11586 
11587 	/// 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.
11588 	mixin Emits!(ChangeEvent!bool);
11589 }
11590 
11591 
11592 /++
11593 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
11594 
11595 
11596 	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
11597 
11598 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11599 
11600 	History:
11601 		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.
11602 +/
11603 class Button : MouseActivatedWidget {
11604 	override int heightStretchiness() { return 3; }
11605 	override int widthStretchiness() { return 3; }
11606 
11607 	/++
11608 		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.
11609 
11610 		History:
11611 			Added July 2, 2021
11612 	+/
11613 	public bool triggersOnMultiClick;
11614 
11615 	private string label_;
11616 	private TextAlignment alignment;
11617 	private dchar accelerator;
11618 
11619 	///
11620 	string label() { return label_; }
11621 	///
11622 	void label(string l) {
11623 		label_ = l;
11624 		version(win32_widgets) {
11625 			WCharzBuffer bfr = WCharzBuffer(l);
11626 			SetWindowTextW(hwnd, bfr.ptr);
11627 		} else version(custom_widgets) {
11628 			redraw();
11629 		}
11630 	}
11631 
11632 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
11633 		super.defaultEventHandler_dblclick(ev);
11634 		if(triggersOnMultiClick) {
11635 			if(ev.button == MouseButton.left) {
11636 				auto event = new Event(EventType.triggered, this);
11637 				event.sendDirectly();
11638 			}
11639 		}
11640 	}
11641 
11642 	private Sprite sprite;
11643 	private int displayFlags;
11644 
11645 	/++
11646 		Creates a push button with the given label, which may be an image or some text.
11647 
11648 		Bugs:
11649 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
11650 
11651 		History:
11652 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
11653 
11654 			The button with label and image will respect requests to show both on Windows as
11655 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
11656 	+/
11657 	this(ImageLabel label, Widget parent) {
11658 		version(win32_widgets) {
11659 			// FIXME: use ideal button size instead
11660 			width = 50;
11661 			height = 30;
11662 			super(parent);
11663 
11664 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
11665 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
11666 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
11667 
11668 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
11669 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
11670 
11671 			if(label.image) {
11672 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
11673 
11674 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
11675 			}
11676 
11677 			this.label = label.label;
11678 		} else version(custom_widgets) {
11679 			width = 50;
11680 			height = 30;
11681 			super(parent);
11682 
11683 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
11684 
11685 			if(label.image) {
11686 				this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
11687 				this.displayFlags = label.displayFlags;
11688 			}
11689 
11690 			this.alignment = label.alignment;
11691 		}
11692 	}
11693 
11694 	///
11695 	this(string label, Widget parent) {
11696 		this(ImageLabel(label), parent);
11697 	}
11698 
11699 	override int minHeight() { return defaultLineHeight + 4; }
11700 
11701 	static class Style : Widget.Style {
11702 		override WidgetBackground background() {
11703 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
11704 
11705 			auto pressed = DynamicState.depressed | DynamicState.hover;
11706 			if((widget.dynamicState & pressed) == pressed) {
11707 				return WidgetBackground(cs.depressedButtonColor());
11708 			} else if(widget.dynamicState & DynamicState.hover) {
11709 				return WidgetBackground(cs.hoveringColor());
11710 			} else {
11711 				return WidgetBackground(cs.buttonColor());
11712 			}
11713 		}
11714 
11715 		override FrameStyle borderStyle() {
11716 			auto pressed = DynamicState.depressed | DynamicState.hover;
11717 			if((widget.dynamicState & pressed) == pressed) {
11718 				return FrameStyle.sunk;
11719 			} else {
11720 				return FrameStyle.risen;
11721 			}
11722 
11723 		}
11724 
11725 		override bool variesWithState(ulong dynamicStateFlags) {
11726 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11727 		}
11728 	}
11729 	mixin OverrideStyle!Style;
11730 
11731 	version(custom_widgets)
11732 	override void paint(WidgetPainter painter) {
11733 		painter.drawThemed(delegate Rectangle(const Rectangle bounds) {
11734 			if(sprite) {
11735 				sprite.drawAt(
11736 					painter,
11737 					bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
11738 					Point(0, 0)
11739 				);
11740 			} else {
11741 				painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
11742 			}
11743 			return bounds;
11744 		});
11745 	}
11746 
11747 	override int flexBasisWidth() {
11748 		version(win32_widgets) {
11749 			SIZE size;
11750 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11751 			if(size.cx == 0)
11752 				goto fallback;
11753 			return size.cx + scaleWithDpi(16);
11754 		}
11755 		fallback:
11756 			return scaleWithDpi(cast(int) label.length * 8 + 16);
11757 	}
11758 
11759 	override int flexBasisHeight() {
11760 		version(win32_widgets) {
11761 			SIZE size;
11762 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11763 			if(size.cy == 0)
11764 				goto fallback;
11765 			return size.cy + scaleWithDpi(6);
11766 		}
11767 		fallback:
11768 			return defaultLineHeight + 4;
11769 	}
11770 }
11771 
11772 /++
11773 	A button with a consistent size, suitable for user commands like OK and CANCEL.
11774 +/
11775 class CommandButton : Button {
11776 	this(string label, Widget parent) {
11777 		super(label, parent);
11778 	}
11779 
11780 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
11781 
11782 	override int maxHeight() {
11783 		return defaultLineHeight + 4;
11784 	}
11785 
11786 	override int maxWidth() {
11787 		return defaultLineHeight * 4;
11788 	}
11789 
11790 	override int marginLeft() { return 12; }
11791 	override int marginRight() { return 12; }
11792 	override int marginTop() { return 12; }
11793 	override int marginBottom() { return 12; }
11794 }
11795 
11796 ///
11797 enum ArrowDirection {
11798 	left, ///
11799 	right, ///
11800 	up, ///
11801 	down ///
11802 }
11803 
11804 ///
11805 version(custom_widgets)
11806 class ArrowButton : Button {
11807 	///
11808 	this(ArrowDirection direction, Widget parent) {
11809 		super("", parent);
11810 		this.direction = direction;
11811 		triggersOnMultiClick = true;
11812 	}
11813 
11814 	private ArrowDirection direction;
11815 
11816 	override int minHeight() { return scaleWithDpi(16); }
11817 	override int maxHeight() { return scaleWithDpi(16); }
11818 	override int minWidth() { return scaleWithDpi(16); }
11819 	override int maxWidth() { return scaleWithDpi(16); }
11820 
11821 	override void paint(WidgetPainter painter) {
11822 		super.paint(painter);
11823 
11824 		auto cs = getComputedStyle();
11825 
11826 		painter.outlineColor = cs.foregroundColor;
11827 		painter.fillColor = cs.foregroundColor;
11828 
11829 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
11830 
11831 		final switch(direction) {
11832 			case ArrowDirection.up:
11833 				painter.drawPolygon(
11834 					scaleWithDpi(Point(2, 10) + offset),
11835 					scaleWithDpi(Point(7, 5) + offset),
11836 					scaleWithDpi(Point(12, 10) + offset),
11837 					scaleWithDpi(Point(2, 10) + offset)
11838 				);
11839 			break;
11840 			case ArrowDirection.down:
11841 				painter.drawPolygon(
11842 					scaleWithDpi(Point(2, 6) + offset),
11843 					scaleWithDpi(Point(7, 11) + offset),
11844 					scaleWithDpi(Point(12, 6) + offset),
11845 					scaleWithDpi(Point(2, 6) + offset)
11846 				);
11847 			break;
11848 			case ArrowDirection.left:
11849 				painter.drawPolygon(
11850 					scaleWithDpi(Point(10, 2) + offset),
11851 					scaleWithDpi(Point(5, 7) + offset),
11852 					scaleWithDpi(Point(10, 12) + offset),
11853 					scaleWithDpi(Point(10, 2) + offset)
11854 				);
11855 			break;
11856 			case ArrowDirection.right:
11857 				painter.drawPolygon(
11858 					scaleWithDpi(Point(6, 2) + offset),
11859 					scaleWithDpi(Point(11, 7) + offset),
11860 					scaleWithDpi(Point(6, 12) + offset),
11861 					scaleWithDpi(Point(6, 2) + offset)
11862 				);
11863 			break;
11864 		}
11865 	}
11866 }
11867 
11868 private
11869 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
11870 	int x, y;
11871 	Widget par = c;
11872 	while(par) {
11873 		x += par.x;
11874 		y += par.y;
11875 		par = par.parent;
11876 	}
11877 	return [x, y];
11878 }
11879 
11880 version(win32_widgets)
11881 private
11882 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
11883 // MapWindowPoints?
11884 	int x, y;
11885 	Widget par = c;
11886 	while(par) {
11887 		x += par.x;
11888 		y += par.y;
11889 		par = par.parent;
11890 		if(par !is null && par.useNativeDrawing())
11891 			break;
11892 	}
11893 	return [x, y];
11894 }
11895 
11896 ///
11897 class ImageBox : Widget {
11898 	private MemoryImage image_;
11899 
11900 	override int widthStretchiness() { return 1; }
11901 	override int heightStretchiness() { return 1; }
11902 	override int widthShrinkiness() { return 1; }
11903 	override int heightShrinkiness() { return 1; }
11904 
11905 	override int flexBasisHeight() {
11906 		return image_.height;
11907 	}
11908 
11909 	override int flexBasisWidth() {
11910 		return image_.width;
11911 	}
11912 
11913 	///
11914 	public void setImage(MemoryImage image){
11915 		this.image_ = image;
11916 		if(this.parentWindow && this.parentWindow.win) {
11917 			if(sprite)
11918 				sprite.dispose();
11919 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
11920 		}
11921 		redraw();
11922 	}
11923 
11924 	/// How to fit the image in the box if they aren't an exact match in size?
11925 	enum HowToFit {
11926 		center, /// centers the image, cropping around all the edges as needed
11927 		crop, /// always draws the image in the upper left, cropping the lower right if needed
11928 		// stretch, /// not implemented
11929 	}
11930 
11931 	private Sprite sprite;
11932 	private HowToFit howToFit_;
11933 
11934 	private Color backgroundColor_;
11935 
11936 	///
11937 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
11938 		this.image_ = image;
11939 		this.tabStop = false;
11940 		this.howToFit_ = howToFit;
11941 		this.backgroundColor_ = backgroundColor;
11942 		super(parent);
11943 		updateSprite();
11944 	}
11945 
11946 	/// ditto
11947 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
11948 		this(image, howToFit, Color.transparent, parent);
11949 	}
11950 
11951 	private void updateSprite() {
11952 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
11953 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
11954 		}
11955 	}
11956 
11957 	override void paint(WidgetPainter painter) {
11958 		updateSprite();
11959 		if(backgroundColor_.a) {
11960 			painter.fillColor = backgroundColor_;
11961 			painter.drawRectangle(Point(0, 0), width, height);
11962 		}
11963 		if(howToFit_ == HowToFit.crop)
11964 			sprite.drawAt(painter, Point(0, 0));
11965 		else if(howToFit_ == HowToFit.center) {
11966 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
11967 		}
11968 	}
11969 }
11970 
11971 ///
11972 class TextLabel : Widget {
11973 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
11974 	override int maxHeight() { return minHeight; }
11975 	override int minWidth() { return 32; }
11976 
11977 	override int flexBasisHeight() { return minHeight(); }
11978 	override int flexBasisWidth() { return defaultTextWidth(label); }
11979 
11980 	string label_;
11981 
11982 	/++
11983 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
11984 
11985 		In practice this means a click on the label will focus the `labelFor`. In future versions
11986 		it will also set screen reader hints but that is not yet implemented.
11987 
11988 		History:
11989 			Added October 3, 2021 (dub v10.4)
11990 	+/
11991 	Widget labelFor;
11992 
11993 	///
11994 	@scriptable
11995 	string label() { return label_; }
11996 
11997 	///
11998 	@scriptable
11999 	void label(string l) {
12000 		label_ = l;
12001 		version(win32_widgets) {
12002 			WCharzBuffer bfr = WCharzBuffer(l);
12003 			SetWindowTextW(hwnd, bfr.ptr);
12004 		} else version(custom_widgets)
12005 			redraw();
12006 	}
12007 
12008 	override void defaultEventHandler_click(scope ClickEvent ce) {
12009 		if(this.labelFor !is null)
12010 			this.labelFor.focus();
12011 	}
12012 
12013 	/++
12014 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
12015 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
12016 	+/
12017 	this(string label, TextAlignment alignment, Widget parent) {
12018 		this.label_ = label;
12019 		this.alignment = alignment;
12020 		this.tabStop = false;
12021 		super(parent);
12022 
12023 		version(win32_widgets)
12024 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
12025 	}
12026 
12027 	/// ditto
12028 	this(string label, Widget parent) {
12029 		this(label, TextAlignment.Right, parent);
12030 	}
12031 
12032 	TextAlignment alignment;
12033 
12034 	version(custom_widgets)
12035 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12036 		painter.outlineColor = getComputedStyle().foregroundColor;
12037 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
12038 		return bounds;
12039 	}
12040 
12041 }
12042 
12043 version(custom_widgets)
12044 	private struct etc {
12045 		mixin ExperimentalTextComponent;
12046 	}
12047 
12048 version(win32_widgets)
12049 	alias EditableTextWidgetParent = Widget; ///
12050 else version(custom_widgets) {
12051 	version(trash_text) {
12052 		alias EditableTextWidgetParent = ScrollableWidget; ///
12053 	} else {
12054 		alias EditableTextWidgetParent = Widget;
12055 		version=use_new_text_system;
12056 		import arsd.textlayouter;
12057 	}
12058 } else static assert(0);
12059 
12060 version(use_new_text_system)
12061 class TextDisplayHelper : Widget {
12062 	protected TextLayouter l;
12063 	protected ScrollMessageWidget smw;
12064 
12065 	private const(TextLayouter.State)*[] undoStack;
12066 	private const(TextLayouter.State)*[] redoStack;
12067 
12068 	bool readonly;
12069 	bool caretNavigation; // scroll lock can flip this
12070 	bool singleLine;
12071 	bool acceptsTabInput;
12072 
12073 	private Menu ctx;
12074 	override Menu contextMenu(int x, int y) {
12075 		if(ctx is null) {
12076 			ctx = new Menu("Actions", this);
12077 			if(!readonly) {
12078 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
12079 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
12080 				ctx.addSeparator();
12081 			}
12082 			if(!readonly)
12083 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
12084 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
12085 			if(!readonly)
12086 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
12087 			if(!readonly)
12088 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
12089 			ctx.addSeparator();
12090 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
12091 		}
12092 		return ctx;
12093 	}
12094 
12095 	override void defaultEventHandler_blur(Event ev) {
12096 		super.defaultEventHandler_blur(ev);
12097 		if(l.wasMutated()) {
12098 			auto evt = new ChangeEvent!string(this, &this.content);
12099 			evt.dispatch();
12100 			l.clearWasMutatedFlag();
12101 		}
12102 	}
12103 
12104 	private string content() {
12105 		return l.getTextString();
12106 	}
12107 
12108 	void undo() {
12109 		if(readonly) return;
12110 		if(undoStack.length) {
12111 			auto state = undoStack[$-1];
12112 			undoStack = undoStack[0 .. $-1];
12113 			undoStack.assumeSafeAppend();
12114 			redoStack ~= l.saveState();
12115 			l.restoreState(state);
12116 			adjustScrollbarSizes();
12117 			scrollForCaret();
12118 			redraw();
12119 			stateCheckpoint = true;
12120 		}
12121 	}
12122 
12123 	void redo() {
12124 		if(readonly) return;
12125 		if(redoStack.length) {
12126 			doStateCheckpoint();
12127 			auto state = redoStack[$-1];
12128 			redoStack = redoStack[0 .. $-1];
12129 			redoStack.assumeSafeAppend();
12130 			l.restoreState(state);
12131 			adjustScrollbarSizes();
12132 			scrollForCaret();
12133 			redraw();
12134 			stateCheckpoint = true;
12135 		}
12136 	}
12137 
12138 	void cut() {
12139 		if(readonly) return;
12140 		with(l.selection()) {
12141 			if(!isEmpty()) {
12142 				setClipboardText(parentWindow.win, getContentString());
12143 				doStateCheckpoint();
12144 				replaceContent("");
12145 				adjustScrollbarSizes();
12146 				scrollForCaret();
12147 				this.redraw();
12148 			}
12149 		}
12150 
12151 	}
12152 
12153 	void copy() {
12154 		with(l.selection()) {
12155 			if(!isEmpty()) {
12156 				setClipboardText(parentWindow.win, getContentString());
12157 				this.redraw();
12158 			}
12159 		}
12160 	}
12161 
12162 	void paste() {
12163 		if(readonly) return;
12164 		getClipboardText(parentWindow.win, (txt) {
12165 			doStateCheckpoint();
12166 			l.selection.replaceContent(txt);
12167 			adjustScrollbarSizes();
12168 			scrollForCaret();
12169 			this.redraw();
12170 		});
12171 	}
12172 
12173 	void deleteContentOfSelection() {
12174 		if(readonly) return;
12175 		doStateCheckpoint();
12176 		l.selection.replaceContent("");
12177 		l.selection.setUserXCoordinate();
12178 		adjustScrollbarSizes();
12179 		scrollForCaret();
12180 		redraw();
12181 	}
12182 
12183 	void selectAll() {
12184 		with(l.selection) {
12185 			moveToStartOfDocument();
12186 			setAnchor();
12187 			moveToEndOfDocument();
12188 			setFocus();
12189 		}
12190 		redraw();
12191 	}
12192 
12193 	protected bool stateCheckpoint = true;
12194 
12195 	protected void doStateCheckpoint() {
12196 		if(stateCheckpoint) {
12197 			undoStack ~= l.saveState();
12198 			stateCheckpoint = false;
12199 		}
12200 	}
12201 
12202 	protected void adjustScrollbarSizes() {
12203 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
12204 		auto borderWidth = 2;
12205 		this.smw.setTotalArea(l.width, l.height);
12206 		this.smw.setViewableArea(
12207 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
12208 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
12209 	}
12210 
12211 	protected void scrollForCaret() {
12212 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
12213 		smw.scrollIntoView(l.selection.focusBoundingBox());
12214 	}
12215 
12216 	// FIXME: this should be a theme changed event listener instead
12217 	private BaseVisualTheme currentTheme;
12218 	override void recomputeChildLayout() {
12219 		if(currentTheme is null)
12220 			currentTheme = WidgetPainter.visualTheme;
12221 		if(WidgetPainter.visualTheme !is currentTheme) {
12222 			currentTheme = WidgetPainter.visualTheme;
12223 			auto ds = this.l.defaultStyle;
12224 			if(auto ms = cast(MyTextStyle) ds) {
12225 				auto cs = getComputedStyle();
12226 				auto font = cs.font();
12227 				if(font !is null)
12228 					ms.font_ = font;
12229 				else {
12230 					auto osc = new OperatingSystemFont();
12231 					osc.loadDefault;
12232 					ms.font_ = osc;
12233 				}
12234 			}
12235 		}
12236 		super.recomputeChildLayout();
12237 	}
12238 
12239 	private Point adjustForSingleLine(Point p) {
12240 		if(singleLine)
12241 			return Point(p.x, this.height / 2);
12242 		else
12243 			return p;
12244 	}
12245 
12246 	private bool wordWrapEnabled_;
12247 
12248 	this(TextLayouter l, ScrollMessageWidget parent) {
12249 		this.smw = parent;
12250 
12251 		smw.addDefaultWheelListeners(16, 16, 8);
12252 		smw.movementPerButtonClick(16, 16);
12253 
12254 		this.defaultPadding = Rectangle(2, 2, 2, 2);
12255 
12256 		this.l = l;
12257 		super(parent);
12258 
12259 		smw.addEventListener((scope ScrollEvent se) {
12260 			this.redraw();
12261 		});
12262 
12263 		bool mouseDown;
12264 
12265 		this.addEventListener((scope ResizeEvent re) {
12266 			// FIXME: I should add a method to give this client area width thing
12267 			if(wordWrapEnabled_)
12268 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
12269 
12270 			adjustScrollbarSizes();
12271 			scrollForCaret();
12272 
12273 			this.redraw();
12274 		});
12275 
12276 		this.addEventListener((scope KeyDownEvent kde) {
12277 			switch(kde.key) {
12278 				case Key.Up, Key.Down, Key.Left, Key.Right:
12279 				case Key.Home, Key.End:
12280 					stateCheckpoint = true;
12281 					bool setPosition = false;
12282 					switch(kde.key) {
12283 						case Key.Up: l.selection.moveUp(); break;
12284 						case Key.Down: l.selection.moveDown(); break;
12285 						case Key.Left: l.selection.moveLeft(); setPosition = true; break;
12286 						case Key.Right: l.selection.moveRight(); setPosition = true; break;
12287 						case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
12288 						case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
12289 						default: assert(0);
12290 					}
12291 
12292 					if(kde.shiftKey)
12293 						l.selection.setFocus();
12294 					else
12295 						l.selection.setAnchor();
12296 					if(setPosition)
12297 						l.selection.setUserXCoordinate();
12298 					scrollForCaret();
12299 					redraw();
12300 				break;
12301 				case Key.PageUp, Key.PageDown:
12302 					// FIXME
12303 					scrollForCaret();
12304 				break;
12305 				case Key.Delete:
12306 					if(l.selection.isEmpty()) {
12307 						l.selection.setAnchor();
12308 						l.selection.moveRight();
12309 						l.selection.setFocus();
12310 					}
12311 					deleteContentOfSelection();
12312 					adjustScrollbarSizes();
12313 					scrollForCaret();
12314 				break;
12315 				case Key.Insert:
12316 				break;
12317 				case Key.A:
12318 					if(kde.ctrlKey)
12319 						selectAll();
12320 				break;
12321 				case Key.F:
12322 					// find
12323 				break;
12324 				case Key.Z:
12325 					if(kde.ctrlKey)
12326 						undo();
12327 				break;
12328 				case Key.R:
12329 					if(kde.ctrlKey)
12330 						redo();
12331 				break;
12332 				case Key.X:
12333 					if(kde.ctrlKey)
12334 						cut();
12335 				break;
12336 				case Key.C:
12337 					if(kde.ctrlKey)
12338 						copy();
12339 				break;
12340 				case Key.V:
12341 					if(kde.ctrlKey)
12342 						paste();
12343 				break;
12344 				case Key.F1:
12345 					with(l.selection()) {
12346 						moveToStartOfLine();
12347 						setAnchor();
12348 						moveToEndOfLine();
12349 						moveToIncludeAdjacentEndOfLineMarker();
12350 						setFocus();
12351 						replaceContent("");
12352 					}
12353 
12354 					redraw();
12355 				break;
12356 				/*
12357 				case Key.F2:
12358 					l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
12359 						//(cast(MyTextStyle) old).font,
12360 						font2,
12361 						Color.red)));
12362 					redraw();
12363 				break;
12364 				*/
12365 				case Key.Tab:
12366 					// we process the char event, so don't want to change focus on it
12367 					if(acceptsTabInput)
12368 						kde.preventDefault();
12369 				break;
12370 				default:
12371 			}
12372 		});
12373 
12374 		Point downAt;
12375 
12376 		static if(UsingSimpledisplayX11)
12377 		this.addEventListener((scope ClickEvent ce) {
12378 			if(ce.button == MouseButton.middle) {
12379 				parentWindow.win.getPrimarySelection((txt) {
12380 					l.selection.replaceContent(txt);
12381 					redraw();
12382 				});
12383 			}
12384 		});
12385 
12386 		this.addEventListener((scope MouseDownEvent ce) {
12387 			if(ce.button == MouseButton.left) {
12388 				downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12389 				l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
12390 				l.selection.setAnchor();
12391 				mouseDown = true;
12392 				parentWindow.captureMouse(this);
12393 				this.redraw();
12394 			} else if(ce.button == MouseButton.right) {
12395 				this.showContextMenu(ce.clientX, ce.clientY);
12396 			}
12397 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12398 		});
12399 
12400 		Timer autoscrollTimer;
12401 		int autoscrollDirection;
12402 		int autoscrollAmount;
12403 
12404 		void autoscroll() {
12405 			switch(autoscrollDirection) {
12406 				case 0: smw.scrollUp(autoscrollAmount); break;
12407 				case 1: smw.scrollDown(autoscrollAmount); break;
12408 				case 2: smw.scrollLeft(autoscrollAmount); break;
12409 				case 3: smw.scrollRight(autoscrollAmount); break;
12410 				default: assert(0);
12411 			}
12412 
12413 			this.redraw();
12414 		}
12415 
12416 		void setAutoscrollTimer(int direction, int amount) {
12417 			if(autoscrollTimer is null) {
12418 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
12419 			}
12420 
12421 			autoscrollDirection = direction;
12422 			autoscrollAmount = amount;
12423 		}
12424 
12425 		void stopAutoscrollTimer() {
12426 			if(autoscrollTimer !is null) {
12427 				autoscrollTimer.dispose();
12428 				autoscrollTimer = null;
12429 			}
12430 			autoscrollAmount = 0;
12431 			autoscrollDirection = 0;
12432 		}
12433 
12434 		this.addEventListener((scope MouseMoveEvent ce) {
12435 			if(mouseDown) {
12436 				auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12437 
12438 				// FIXME: when scrolling i actually do want a timer.
12439 				// i also want a zone near the sides of the window where i can auto scroll
12440 
12441 				auto scrollMultiplier = scaleWithDpi(16);
12442 				auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
12443 
12444 				if(!singleLine && movedTo.y < 4) {
12445 					setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
12446 				} else
12447 				if(!singleLine && (movedTo.y + 6) > this.height) {
12448 					setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
12449 				} else
12450 				if(movedTo.x < 4) {
12451 					setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
12452 				} else
12453 				if((movedTo.x + 6) > this.width) {
12454 					setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
12455 				} else
12456 					stopAutoscrollTimer();
12457 
12458 				l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
12459 				l.selection.setFocus();
12460 				this.redraw();
12461 			}
12462 		});
12463 
12464 		this.addEventListener((scope MouseUpEvent ce) {
12465 			// FIXME: assert primary selection
12466 			if(mouseDown && ce.button == MouseButton.left) {
12467 				stateCheckpoint = true;
12468 				//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
12469 				//l.selection.setFocus();
12470 				mouseDown = false;
12471 				parentWindow.releaseMouseCapture();
12472 				stopAutoscrollTimer();
12473 				this.redraw();
12474 			}
12475 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12476 		});
12477 
12478 		this.addEventListener((scope CharEvent ce) {
12479 			if(readonly)
12480 				return;
12481 			if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
12482 				return; // skip the ctrl+x characters we don't care about as plain text
12483 
12484 			if(singleLine && ce.character == '\n')
12485 				return;
12486 			if(!acceptsTabInput && ce.character == '\t')
12487 				return;
12488 
12489 			doStateCheckpoint();
12490 
12491 			char[4] buffer;
12492 			import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web
12493 			auto stride = encode(buffer, ce.character);
12494 			l.selection.replaceContent(buffer[0 .. stride]);
12495 			l.selection.setUserXCoordinate();
12496 			adjustScrollbarSizes();
12497 			scrollForCaret();
12498 			redraw();
12499 		});
12500 	}
12501 
12502 	static class Style : Widget.Style {
12503 		override WidgetBackground background() {
12504 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
12505 		}
12506 
12507 		override Color foregroundColor() {
12508 			return WidgetPainter.visualTheme.foregroundColor;
12509 		}
12510 
12511 		override FrameStyle borderStyle() {
12512 			return FrameStyle.sunk;
12513 		}
12514 
12515 		override MouseCursor cursor() {
12516 			return GenericCursor.Text;
12517 		}
12518 	}
12519 	mixin OverrideStyle!Style;
12520 
12521 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
12522 	override int maxHeight() {
12523 		if(singleLine)
12524 			return minHeight;
12525 		else
12526 			return super.maxHeight();
12527 	}
12528 
12529 	void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
12530 		painter.drawText(upperLeft, text);
12531 	}
12532 
12533 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12534 		//painter.setFont(font);
12535 
12536 		auto cs = getComputedStyle();
12537 		auto defaultColor = cs.foregroundColor;
12538 
12539 		auto old = painter.setClipRectangle(bounds);
12540 		scope(exit) painter.setClipRectangle(old);
12541 
12542 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
12543 			//writeln("Segment: ", txt);
12544 			assert(style !is null);
12545 
12546 			auto myStyle = cast(MyTextStyle) style;
12547 			assert(myStyle !is null);
12548 
12549 			painter.setFont(myStyle.font);
12550 			// defaultColor = myStyle.color; // FIXME: so wrong
12551 
12552 			if(info.selections && info.boundingBox.width > 0) {
12553 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
12554 				painter.fillColor = color;
12555 				painter.outlineColor = color;
12556 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
12557 				painter.outlineColor = cs.selectionForegroundColor;
12558 				//painter.fillColor = Color.white;
12559 			} else {
12560 				painter.outlineColor = defaultColor;
12561 			}
12562 
12563 			if(this.isFocused)
12564 			foreach(idx, caret; carets) {
12565 				if(idx == 0)
12566 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
12567 				painter.drawLine(
12568 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
12569 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
12570 				);
12571 			}
12572 
12573 			if(txt.stripInternal.length) {
12574 				drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
12575 			}
12576 
12577 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
12578 				return false;
12579 			} else {
12580 				return true;
12581 			}
12582 		}, Rectangle(smw.position(), bounds.size));
12583 
12584 		/+
12585 		int place = 0;
12586 		int y = 75;
12587 		foreach(width; widths) {
12588 			painter.fillColor = Color.red;
12589 			painter.drawRectangle(Point(place, y), Size(width, 75));
12590 			//y += 15;
12591 			place += width;
12592 		}
12593 		+/
12594 
12595 		return bounds;
12596 	}
12597 
12598 	static class MyTextStyle : TextStyle {
12599 		OperatingSystemFont font_;
12600 		this(OperatingSystemFont font, bool passwordMode = false) {
12601 			this.font_ = font;
12602 		}
12603 
12604 		override OperatingSystemFont font() {
12605 			return font_;
12606 		}
12607 	}
12608 }
12609 
12610 /+
12611 version(use_new_text_system)
12612 class TextWidget : Widget {
12613 	TextLayouter l;
12614 	ScrollMessageWidget smw;
12615 	TextDisplayHelper helper;
12616 	this(TextLayouter l, Widget parent) {
12617 		this.l = l;
12618 		super(parent);
12619 
12620 		smw = new ScrollMessageWidget(this);
12621 		//smw.horizontalScrollBar.hide;
12622 		//smw.verticalScrollBar.hide;
12623 		smw.addDefaultWheelListeners(16, 16, 8);
12624 		smw.movementPerButtonClick(16, 16);
12625 		helper = new TextDisplayHelper(l, smw);
12626 
12627 		// no need to do this here since there's gonna be a resize
12628 		// event immediately before any drawing
12629 		// smw.setTotalArea(l.width, l.height);
12630 		smw.setViewableArea(
12631 			this.width - this.paddingLeft - this.paddingRight,
12632 			this.height - this.paddingTop - this.paddingBottom);
12633 
12634 		/+
12635 		writeln(l.width, "x", l.height);
12636 		+/
12637 	}
12638 }
12639 +/
12640 
12641 
12642 
12643 
12644 /+
12645 	This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too
12646 +/
12647 
12648 /// Contains the implementation of text editing
12649 abstract class EditableTextWidget : EditableTextWidgetParent {
12650 	this(Widget parent) {
12651 		super(parent);
12652 
12653 		version(custom_widgets)
12654 			setupCustomTextEditing();
12655 	}
12656 
12657 	private bool wordWrapEnabled_;
12658 	void wordWrapEnabled(bool enabled) {
12659 		version(win32_widgets) {
12660 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
12661 		} else version(custom_widgets) {
12662 			wordWrapEnabled_ = enabled;
12663 			version(use_new_text_system)
12664 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
12665 		} else static assert(false);
12666 	}
12667 
12668 	override int minWidth() { return scaleWithDpi(16); }
12669 	override int widthStretchiness() { return 7; }
12670 	override int widthShrinkiness() { return 1; }
12671 
12672 	version(use_new_text_system)
12673 	override int maxHeight() { return tdh.maxHeight; }
12674 
12675 	version(use_new_text_system)
12676 	override void focus() { if(tdh) tdh.focus(); else super.focus(); }
12677 
12678 	void selectAll() {
12679 		version(win32_widgets)
12680 			SendMessage(hwnd, EM_SETSEL, 0, -1);
12681 		else version(custom_widgets) {
12682 			version(use_new_text_system)
12683 				tdh.selectAll();
12684 			else
12685 				textLayout.selectAll();
12686 			redraw();
12687 		}
12688 	}
12689 
12690 	version(use_new_text_system)
12691 		TextDisplayHelper tdh;
12692 
12693 	@property string content() {
12694 		version(win32_widgets) {
12695 			wchar[4096] bufferstack;
12696 			wchar[] buffer;
12697 			auto len = GetWindowTextLength(hwnd);
12698 			if(len < bufferstack.length)
12699 				buffer = bufferstack[0 .. len + 1];
12700 			else
12701 				buffer = new wchar[](len + 1);
12702 
12703 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
12704 			if(l >= 0)
12705 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
12706 			else
12707 				return null;
12708 		} else version(custom_widgets) {
12709 			version(use_new_text_system) {
12710 				return textLayout.getTextString();
12711 			} else
12712 				return textLayout.getPlainText();
12713 		} else static assert(false);
12714 	}
12715 	@property void content(string s) {
12716 		version(win32_widgets) {
12717 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
12718 			SetWindowTextW(hwnd, bfr.ptr);
12719 		} else version(custom_widgets) {
12720 			version(use_new_text_system) {
12721 				selectAll();
12722 				textLayout.selection.replaceContent(s);
12723 
12724 				tdh.adjustScrollbarSizes();
12725 				// these don't seem to help
12726 				// tdh.smw.setPosition(0, 0);
12727 				// tdh.scrollForCaret();
12728 
12729 				redraw();
12730 			} else {
12731 				textLayout.clear();
12732 				textLayout.addText(s);
12733 
12734 				{
12735 				// FIXME: it should be able to get this info easier
12736 				auto painter = draw();
12737 				textLayout.redoLayout(painter);
12738 				}
12739 				auto cbb = textLayout.contentBoundingBox();
12740 				setContentSize(cbb.width, cbb.height);
12741 				/*
12742 				textLayout.addText(ForegroundColor.red, s);
12743 				textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/");
12744 				textLayout.addText(" is the best!");
12745 				*/
12746 				redraw();
12747 			}
12748 		}
12749 		else static assert(false);
12750 	}
12751 
12752 	void addText(string txt) {
12753 		version(custom_widgets) {
12754 			version(use_new_text_system) {
12755 				textLayout.appendText(txt);
12756 				tdh.adjustScrollbarSizes();
12757 				redraw();
12758 			} else {
12759 				textLayout.addText(txt);
12760 
12761 				{
12762 				// FIXME: it should be able to get this info easier
12763 				auto painter = draw();
12764 				textLayout.redoLayout(painter);
12765 				}
12766 				auto cbb = textLayout.contentBoundingBox();
12767 				setContentSize(cbb.width, cbb.height);
12768 			}
12769 		} else version(win32_widgets) {
12770 			// get the current selection
12771 			DWORD StartPos, EndPos;
12772 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
12773 
12774 			// move the caret to the end of the text
12775 			int outLength = GetWindowTextLengthW(hwnd);
12776 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
12777 
12778 			// insert the text at the new caret position
12779 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
12780 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
12781 
12782 			// restore the previous selection
12783 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
12784 		} else static assert(0);
12785 	}
12786 
12787 	version(custom_widgets)
12788 	version(trash_text)
12789 	override void paintFrameAndBackground(WidgetPainter painter) {
12790 		this.draw3dFrame(painter, FrameStyle.sunk, Color.white);
12791 	}
12792 
12793 	version(use_new_text_system)
12794 	TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
12795 		return new TextDisplayHelper(textLayout, smw);
12796 	}
12797 
12798 	version(use_new_text_system)
12799 	TextStyle defaultTextStyle() {
12800 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
12801 	}
12802 
12803 	version(use_new_text_system)
12804 	private OperatingSystemFont getUsedFont() {
12805 		auto cs = getComputedStyle();
12806 		auto font = cs.font;
12807 		if(font is null) {
12808 			font = new OperatingSystemFont;
12809 			font.loadDefault();
12810 		}
12811 		return font;
12812 	}
12813 
12814 	version(win32_widgets) { /* will do it with Windows calls in the classes */ }
12815 	else version(custom_widgets) {
12816 		// FIXME
12817 		version(use_new_text_system) {
12818 			TextLayouter textLayout;
12819 
12820 			void setupCustomTextEditing() {
12821 				textLayout = new TextLayouter(defaultTextStyle());
12822 				auto smw = new ScrollMessageWidget(this);
12823 				if(!showingHorizontalScroll)
12824 					smw.horizontalScrollBar.hide();
12825 				if(!showingVerticalScroll)
12826 					smw.verticalScrollBar.hide();
12827 				this.tabStop = false;
12828 				smw.tabStop = false;
12829 				tdh = textDisplayHelperFactory(textLayout, smw);
12830 
12831 				this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
12832 					if(textLayout) {
12833 						if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
12834 							// the dpi change can change the font, so this informs the layouter that it has changed too
12835 							style.font_ = getUsedFont();
12836 
12837 							// arsd.core.writeln(this.parentWindow.win.actualDpi);
12838 						}
12839 					}
12840 				});
12841 			}
12842 
12843 		} else {
12844 
12845 			static if(SimpledisplayTimerAvailable)
12846 				Timer caretTimer;
12847 			etc.TextLayout textLayout;
12848 
12849 			void setupCustomTextEditing() {
12850 				textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4));
12851 				textLayout.selectionXorColor = getComputedStyle().activeListXorColor;
12852 			}
12853 
12854 			override void paint(WidgetPainter painter) {
12855 				if(parentWindow.win.closed) return;
12856 
12857 				textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4);
12858 
12859 				/*
12860 				painter.outlineColor = Color.white;
12861 				painter.fillColor = Color.white;
12862 				painter.drawRectangle(Point(4, 4), contentWidth, contentHeight);
12863 				*/
12864 
12865 				painter.outlineColor = Color.black;
12866 				// painter.drawText(Point(4, 4), content, Point(width - 4, height - 4));
12867 
12868 				textLayout.caretShowingOnScreen = false;
12869 
12870 				textLayout.drawInto(painter, !parentWindow.win.closed && isFocused());
12871 			}
12872 		}
12873 
12874 		static class Style : Widget.Style {
12875 			override FrameStyle borderStyle() {
12876 				return FrameStyle.sunk;
12877 			}
12878 			override MouseCursor cursor() {
12879 				return GenericCursor.Text;
12880 			}
12881 		}
12882 		mixin OverrideStyle!Style;
12883 	}
12884 	else static assert(false);
12885 
12886 	version(trash_text)
12887 	version(custom_widgets)
12888 	override void defaultEventHandler_mousedown(MouseDownEvent ev) {
12889 		super.defaultEventHandler_mousedown(ev);
12890 		if(parentWindow.win.closed) return;
12891 		if(ev.button == MouseButton.left) {
12892 			if(textLayout.selectNone())
12893 				redraw();
12894 			textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY);
12895 			this.focus();
12896 			//this.parentWindow.win.grabInput();
12897 		} else if(ev.button == MouseButton.middle) {
12898 			static if(UsingSimpledisplayX11) {
12899 				getPrimarySelection(parentWindow.win, (in char[] txt) {
12900 					textLayout.insert(txt);
12901 					redraw();
12902 
12903 					auto cbb = textLayout.contentBoundingBox();
12904 					setContentSize(cbb.width, cbb.height);
12905 				});
12906 			}
12907 		}
12908 	}
12909 
12910 	version(trash_text)
12911 	version(custom_widgets)
12912 	override void defaultEventHandler_mouseup(MouseUpEvent ev) {
12913 		//this.parentWindow.win.releaseInputGrab();
12914 		super.defaultEventHandler_mouseup(ev);
12915 	}
12916 
12917 	version(trash_text)
12918 	version(custom_widgets)
12919 	override void defaultEventHandler_mousemove(MouseMoveEvent ev) {
12920 		super.defaultEventHandler_mousemove(ev);
12921 		if(ev.state & ModifierState.leftButtonDown) {
12922 			textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY);
12923 			redraw();
12924 		}
12925 	}
12926 
12927 	version(trash_text)
12928 	version(custom_widgets)
12929 	override void defaultEventHandler_focus(Event ev) {
12930 		super.defaultEventHandler_focus(ev);
12931 		if(parentWindow.win.closed) return;
12932 		auto painter = this.draw();
12933 		textLayout.drawCaret(painter);
12934 
12935 		static if(SimpledisplayTimerAvailable)
12936 		if(caretTimer) {
12937 			caretTimer.destroy();
12938 			caretTimer = null;
12939 		}
12940 
12941 		bool blinkingCaret = true;
12942 		static if(UsingSimpledisplayX11)
12943 			if(!Image.impl.xshmAvailable)
12944 				blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink
12945 
12946 		if(blinkingCaret)
12947 		static if(SimpledisplayTimerAvailable)
12948 		caretTimer = new Timer(500, {
12949 			if(parentWindow.win.closed) {
12950 				caretTimer.destroy();
12951 				return;
12952 			}
12953 			if(isFocused()) {
12954 				auto painter = this.draw();
12955 				textLayout.drawCaret(painter);
12956 			} else if(textLayout.caretShowingOnScreen) {
12957 				auto painter = this.draw();
12958 				textLayout.eraseCaret(painter);
12959 			}
12960 		});
12961 	}
12962 
12963 	version(trash_text) {
12964 		private string lastContentBlur;
12965 
12966 		override void defaultEventHandler_blur(Event ev) {
12967 			super.defaultEventHandler_blur(ev);
12968 			if(parentWindow.win.closed) return;
12969 			version(custom_widgets) {
12970 				auto painter = this.draw();
12971 				textLayout.eraseCaret(painter);
12972 				static if(SimpledisplayTimerAvailable)
12973 				if(caretTimer) {
12974 					caretTimer.destroy();
12975 					caretTimer = null;
12976 				}
12977 			}
12978 
12979 			if(this.content != lastContentBlur) {
12980 				auto evt = new ChangeEvent!string(this, &this.content);
12981 				evt.dispatch();
12982 				lastContentBlur = this.content;
12983 			}
12984 		}
12985 	}
12986 
12987 	version(win32_widgets) {
12988 		private string lastContentBlur;
12989 
12990 		override void defaultEventHandler_blur(Event ev) {
12991 			super.defaultEventHandler_blur(ev);
12992 
12993 			if(this.content != lastContentBlur) {
12994 				auto evt = new ChangeEvent!string(this, &this.content);
12995 				evt.dispatch();
12996 				lastContentBlur = this.content;
12997 			}
12998 		}
12999 	}
13000 
13001 
13002 	version(trash_text)
13003 	version(custom_widgets)
13004 	override void defaultEventHandler_char(CharEvent ev) {
13005 		super.defaultEventHandler_char(ev);
13006 		textLayout.insert(ev.character);
13007 		redraw();
13008 
13009 		// FIXME: too inefficient
13010 		auto cbb = textLayout.contentBoundingBox();
13011 		setContentSize(cbb.width, cbb.height);
13012 	}
13013 	version(trash_text)
13014 	version(custom_widgets)
13015 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
13016 		//super.defaultEventHandler_keydown(ev);
13017 		switch(ev.key) {
13018 			case Key.Delete:
13019 				textLayout.delete_();
13020 				redraw();
13021 			break;
13022 			case Key.Left:
13023 				textLayout.moveLeft();
13024 				redraw();
13025 			break;
13026 			case Key.Right:
13027 				textLayout.moveRight();
13028 				redraw();
13029 			break;
13030 			case Key.Up:
13031 				textLayout.moveUp();
13032 				redraw();
13033 			break;
13034 			case Key.Down:
13035 				textLayout.moveDown();
13036 				redraw();
13037 			break;
13038 			case Key.Home:
13039 				textLayout.moveHome();
13040 				redraw();
13041 			break;
13042 			case Key.End:
13043 				textLayout.moveEnd();
13044 				redraw();
13045 			break;
13046 			case Key.PageUp:
13047 				foreach(i; 0 .. 32)
13048 				textLayout.moveUp();
13049 				redraw();
13050 			break;
13051 			case Key.PageDown:
13052 				foreach(i; 0 .. 32)
13053 				textLayout.moveDown();
13054 				redraw();
13055 			break;
13056 
13057 			default:
13058 				 {} // intentionally blank, let "char" handle it
13059 		}
13060 		/*
13061 		if(ev.key == Key.Backspace) {
13062 			textLayout.backspace();
13063 			redraw();
13064 		}
13065 		*/
13066 		ensureVisibleInScroll(textLayout.caretBoundingBox());
13067 	}
13068 
13069 	version(use_new_text_system) {
13070 		bool showingVerticalScroll() { return true; }
13071 		bool showingHorizontalScroll() { return true; }
13072 	}
13073 }
13074 
13075 ///
13076 class LineEdit : EditableTextWidget {
13077 	// FIXME: hack
13078 	version(custom_widgets) {
13079 	override bool showingVerticalScroll() { return false; }
13080 	override bool showingHorizontalScroll() { return false; }
13081 	}
13082 
13083 	override int flexBasisWidth() { return 250; }
13084 
13085 	///
13086 	this(Widget parent) {
13087 		super(parent);
13088 		version(win32_widgets) {
13089 			createWin32Window(this, "edit"w, "",
13090 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13091 		} else version(custom_widgets) {
13092 			version(trash_text) {
13093 				setupCustomTextEditing();
13094 				addEventListener(delegate(CharEvent ev) {
13095 					if(ev.character == '\n')
13096 						ev.preventDefault();
13097 				});
13098 			}
13099 		} else static assert(false);
13100 	}
13101 
13102 	version(use_new_text_system)
13103 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13104 		auto tdh = new TextDisplayHelper(textLayout, smw);
13105 		tdh.singleLine = true;
13106 		return tdh;
13107 	}
13108 
13109 	version(win32_widgets) {
13110 		mixin Padding!q{0};
13111 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13112 		override int maxHeight() { return minHeight; }
13113 	}
13114 
13115 	/+
13116 	@property void passwordMode(bool p) {
13117 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
13118 	}
13119 	+/
13120 }
13121 
13122 /++
13123 	A [LineEdit] that displays `*` in place of the actual characters.
13124 
13125 	Alas, Windows requires the window to be created differently to use this style,
13126 	so it had to be a new class instead of a toggle on and off on an existing object.
13127 
13128 	FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now.
13129 
13130 	History:
13131 		Added January 24, 2021
13132 +/
13133 class PasswordEdit : EditableTextWidget {
13134 	version(custom_widgets) {
13135 	override bool showingVerticalScroll() { return false; }
13136 	override bool showingHorizontalScroll() { return false; }
13137 	}
13138 
13139 	override int flexBasisWidth() { return 250; }
13140 
13141 	version(use_new_text_system)
13142 	override TextStyle defaultTextStyle() {
13143 		auto cs = getComputedStyle();
13144 
13145 		auto osf = new class OperatingSystemFont {
13146 			this() {
13147 				super(cs.font);
13148 			}
13149 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
13150 				int count = 0;
13151 				foreach(dchar ch; text)
13152 					count++;
13153 				return count * super.stringWidth("*", window);
13154 			}
13155 		};
13156 
13157 		return new TextDisplayHelper.MyTextStyle(osf);
13158 	}
13159 
13160 	version(use_new_text_system)
13161 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13162 		static class TDH : TextDisplayHelper {
13163 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
13164 				singleLine = true;
13165 				super(textLayout, smw);
13166 			}
13167 
13168 			override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
13169 				char[256] buffer = void;
13170 				int bufferLength = 0;
13171 				foreach(dchar ch; text)
13172 					buffer[bufferLength++] = '*';
13173 				painter.drawText(upperLeft, buffer[0..bufferLength]);
13174 			}
13175 		}
13176 
13177 		return new TDH(textLayout, smw);
13178 	}
13179 
13180 	///
13181 	this(Widget parent) {
13182 		super(parent);
13183 		version(win32_widgets) {
13184 			createWin32Window(this, "edit"w, "",
13185 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13186 		} else version(custom_widgets) {
13187 			version(trash_text)
13188 			setupCustomTextEditing();
13189 			addEventListener(delegate(CharEvent ev) {
13190 				if(ev.character == '\n')
13191 					ev.preventDefault();
13192 			});
13193 		} else static assert(false);
13194 	}
13195 	version(win32_widgets) {
13196 		mixin Padding!q{2};
13197 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13198 		override int maxHeight() { return minHeight; }
13199 	}
13200 }
13201 
13202 ///
13203 class TextEdit : EditableTextWidget {
13204 	///
13205 	this(Widget parent) {
13206 		super(parent);
13207 		version(win32_widgets) {
13208 			createWin32Window(this, "edit"w, "",
13209 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
13210 		} else version(custom_widgets) {
13211 			version(trash_text)
13212 			setupCustomTextEditing();
13213 		} else static assert(false);
13214 	}
13215 	override int maxHeight() { return int.max; }
13216 	override int heightStretchiness() { return 7; }
13217 
13218 	override int flexBasisWidth() { return 250; }
13219 	override int flexBasisHeight() { return 25; }
13220 }
13221 
13222 
13223 /+
13224 /++
13225 
13226 +/
13227 version(none)
13228 class RichTextDisplay : Widget {
13229 	@property void content(string c) {}
13230 	void appendContent(string c) {}
13231 }
13232 +/
13233 
13234 /++
13235 	A read-only text display
13236 
13237 	History:
13238 		Added October 31, 2023 (dub v11.3)
13239 +/
13240 class TextDisplay : EditableTextWidget {
13241 	this(string text, Widget parent) {
13242 		super(parent);
13243 		this.content = text;
13244 	}
13245 
13246 	override int maxHeight() { return int.max; }
13247 	override int minHeight() { return 50; }
13248 	override int heightStretchiness() { return 7; }
13249 
13250 	override int flexBasisWidth() { return 250; }
13251 	override int flexBasisHeight() { return 50; }
13252 
13253 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13254 		return new MyTextDisplayHelper(textLayout, smw);
13255 	}
13256 
13257 	override void registerMovement() {
13258 		super.registerMovement();
13259 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
13260 	}
13261 
13262 	static class MyTextDisplayHelper : TextDisplayHelper {
13263 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
13264 			smw.verticalScrollBar.hide();
13265 			smw.horizontalScrollBar.hide();
13266 			super(textLayout, smw);
13267 			this.readonly = true;
13268 		}
13269 
13270 		override void registerMovement() {
13271 			super.registerMovement();
13272 
13273 			// FIXME: do the horizontal one too as needed and make sure that it does
13274 			// wordwrapping again
13275 			if(l.height + smw.horizontalScrollBar.height > this.height)
13276 				smw.verticalScrollBar.show();
13277 			else
13278 				smw.verticalScrollBar.hide();
13279 
13280 			l.wordWrapWidth = this.width;
13281 
13282 			smw.verticalScrollBar.setPosition = 0;
13283 		}
13284 
13285 		class Style : Widget.Style {
13286 			// just want the generic look for these
13287 		}
13288 
13289 		mixin OverrideStyle!Style;
13290 	}
13291 }
13292 
13293 ///
13294 class MessageBox : Window {
13295 	private string message;
13296 	MessageBoxButton buttonPressed = MessageBoxButton.None;
13297 	///
13298 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
13299 		super(300, 100);
13300 
13301 		assert(buttons.length);
13302 		assert(buttons.length ==  buttonIds.length);
13303 
13304 		this.message = message;
13305 
13306 		auto label = new TextDisplay(message, this);
13307 
13308 		auto hl = new HorizontalLayout(this);
13309 		auto spacer = new HorizontalSpacer(hl); // to right align
13310 
13311 		foreach(idx, buttonText; buttons) {
13312 			auto button = new CommandButton(buttonText, hl);
13313 
13314 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
13315 				this.buttonPressed = buttonIds[idx];
13316 				win.close();
13317 			}; })(idx));
13318 
13319 			if(idx == 0)
13320 				button.focus();
13321 		}
13322 
13323 		if(buttons.length == 1)
13324 			auto spacer2 = new HorizontalSpacer(hl); // to center it
13325 
13326 		win.resize(scaleWithDpi(300), this.minHeight());
13327 
13328 		win.show();
13329 		redraw();
13330 	}
13331 
13332 	mixin Padding!q{16};
13333 }
13334 
13335 ///
13336 enum MessageBoxStyle {
13337 	OK, ///
13338 	OKCancel, ///
13339 	RetryCancel, ///
13340 	YesNo, ///
13341 	YesNoCancel, ///
13342 	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.
13343 }
13344 
13345 ///
13346 enum MessageBoxIcon {
13347 	None, ///
13348 	Info, ///
13349 	Warning, ///
13350 	Error ///
13351 }
13352 
13353 /// Identifies the button the user pressed on a message box.
13354 enum MessageBoxButton {
13355 	None, /// The user closed the message box without clicking any of the buttons.
13356 	OK, ///
13357 	Cancel, ///
13358 	Retry, ///
13359 	Yes, ///
13360 	No, ///
13361 	Continue ///
13362 }
13363 
13364 
13365 /++
13366 	Displays a modal message box, blocking until the user dismisses it.
13367 
13368 	Returns: the button pressed.
13369 +/
13370 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13371 	version(win32_widgets) {
13372 		WCharzBuffer t = WCharzBuffer(title);
13373 		WCharzBuffer m = WCharzBuffer(message);
13374 		UINT type;
13375 		with(MessageBoxStyle)
13376 		final switch(style) {
13377 			case OK: type |= MB_OK; break;
13378 			case OKCancel: type |= MB_OKCANCEL; break;
13379 			case RetryCancel: type |= MB_RETRYCANCEL; break;
13380 			case YesNo: type |= MB_YESNO; break;
13381 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
13382 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
13383 		}
13384 		with(MessageBoxIcon)
13385 		final switch(icon) {
13386 			case None: break;
13387 			case Info: type |= MB_ICONINFORMATION; break;
13388 			case Warning: type |= MB_ICONWARNING; break;
13389 			case Error: type |= MB_ICONERROR; break;
13390 		}
13391 		switch(MessageBoxW(null, m.ptr, t.ptr, type)) {
13392 			case IDOK: return MessageBoxButton.OK;
13393 			case IDCANCEL: return MessageBoxButton.Cancel;
13394 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
13395 			case IDYES: return MessageBoxButton.Yes;
13396 			case IDNO: return MessageBoxButton.No;
13397 			case IDCONTINUE: return MessageBoxButton.Continue;
13398 			default: return MessageBoxButton.None;
13399 		}
13400 	} else {
13401 		string[] buttons;
13402 		MessageBoxButton[] buttonIds;
13403 		with(MessageBoxStyle)
13404 		final switch(style) {
13405 			case OK:
13406 				buttons = ["OK"];
13407 				buttonIds = [MessageBoxButton.OK];
13408 			break;
13409 			case OKCancel:
13410 				buttons = ["OK", "Cancel"];
13411 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
13412 			break;
13413 			case RetryCancel:
13414 				buttons = ["Retry", "Cancel"];
13415 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
13416 			break;
13417 			case YesNo:
13418 				buttons = ["Yes", "No"];
13419 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
13420 			break;
13421 			case YesNoCancel:
13422 				buttons = ["Yes", "No", "Cancel"];
13423 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
13424 			break;
13425 			case RetryCancelContinue:
13426 				buttons = ["Try Again", "Cancel", "Continue"];
13427 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
13428 			break;
13429 		}
13430 		auto mb = new MessageBox(message, buttons, buttonIds);
13431 		EventLoop el = EventLoop.get;
13432 		el.run(() { return !mb.win.closed; });
13433 		return mb.buttonPressed;
13434 	}
13435 }
13436 
13437 /// ditto
13438 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13439 	return messageBox(null, message, style, icon);
13440 }
13441 
13442 
13443 
13444 ///
13445 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
13446 
13447 /++
13448 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
13449 
13450 	History:
13451 		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.
13452 +/
13453 struct EventListener {
13454 	private Widget widget;
13455 	private string event;
13456 	private EventHandler handler;
13457 	private bool useCapture;
13458 
13459 	///
13460 	void disconnect() {
13461 		widget.removeEventListener(this);
13462 	}
13463 }
13464 
13465 /++
13466 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
13467 
13468 	Now, I recommend you use a statically typed event object instead.
13469 
13470 	See_Also: [Event]
13471 +/
13472 enum EventType : string {
13473 	click = "click", ///
13474 
13475 	mouseenter = "mouseenter", ///
13476 	mouseleave = "mouseleave", ///
13477 	mousein = "mousein", ///
13478 	mouseout = "mouseout", ///
13479 	mouseup = "mouseup", ///
13480 	mousedown = "mousedown", ///
13481 	mousemove = "mousemove", ///
13482 
13483 	keydown = "keydown", ///
13484 	keyup = "keyup", ///
13485 	char_ = "char", ///
13486 
13487 	focus = "focus", ///
13488 	blur = "blur", ///
13489 
13490 	triggered = "triggered", ///
13491 
13492 	change = "change", ///
13493 }
13494 
13495 /++
13496 	Represents an event that is currently being processed.
13497 
13498 
13499 	Minigui's event model is based on the web browser. An event has a name, a target,
13500 	and an associated data object. It starts from the window and works its way down through
13501 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
13502 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
13503 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
13504 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
13505 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
13506 	whenever propagation is done, not only if it gets to the end of the chain).
13507 
13508 	This model has several nice points:
13509 
13510 	$(LIST
13511 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
13512 		  with event handlers set, then add/remove children as much as you want without needing
13513 		  to manage the event handlers on them - the parent alone can manage everything.
13514 
13515 		* It is easy to create new custom events in your application.
13516 
13517 		* It is familiar to many web developers.
13518 	)
13519 
13520 	There's a few downsides though:
13521 
13522 	$(LIST
13523 		* There's not a lot of type safety.
13524 
13525 		* You don't get a static list of what events a widget can emit.
13526 
13527 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
13528 		  the central delegation benefit is it can be lead to debugging of action at a distance.
13529 	)
13530 
13531 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
13532 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
13533 	to simply use a D object type which provides a static interface as well as a built-in event name.
13534 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
13535 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
13536 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
13537 	to having a little more help from the D compiler and documentation generator.
13538 
13539 	Your code would change like this:
13540 
13541 	---
13542 	// old
13543 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
13544 
13545 	// new
13546 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
13547 	---
13548 
13549 	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.
13550 
13551 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
13552 
13553 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
13554 
13555 	Thus the family of functions are:
13556 
13557 	[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.
13558 
13559 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
13560 
13561 	[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.
13562 
13563 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
13564 
13565 	---
13566 	class MyCheckbox : Widget {
13567 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
13568 		/// It is NOT actually required but should be used whenever possible.
13569 		mixin Emits!(ChangeEvent!bool);
13570 
13571 		this(Widget parent) {
13572 			super(parent);
13573 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
13574 		}
13575 
13576 		private bool _checked;
13577 		@property bool checked() { return _checked; }
13578 		@property void checked(bool set) {
13579 			_checked = set;
13580 			emit!(ChangeEvent!bool)(&checked);
13581 		}
13582 	}
13583 	---
13584 
13585 	## Creating Your Own Events
13586 
13587 	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.
13588 
13589 	---
13590 	class MyEvent : Event {
13591 		this(Widget target) { super(EventString, target); }
13592 		mixin Register; // adds EventString and other reflection information
13593 	}
13594 	---
13595 
13596 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
13597 
13598 	History:
13599 		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.
13600 
13601 		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.
13602 +/
13603 /+
13604 
13605 	## General Conventions
13606 
13607 	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.
13608 
13609 
13610 	## Qt-style signals and slots
13611 
13612 	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.
13613 
13614 	The intention is for events to be used when
13615 
13616 	---
13617 	class Demo : Widget {
13618 		this() {
13619 			myPropertyChanged = Signal!int(this);
13620 		}
13621 		@property myProperty(int v) {
13622 			myPropertyChanged.emit(v);
13623 		}
13624 
13625 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
13626 		// but it can just genuinely not care about `this` since that's not really passed.
13627 	}
13628 
13629 	class Foo : Widget {
13630 		// the slot uda is not necessary, but it helps the script and ui builder find it.
13631 		@slot void setValue(int v) { ... }
13632 	}
13633 
13634 	demo.myPropertyChanged.connect(&foo.setValue);
13635 	---
13636 
13637 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
13638 
13639 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
13640 
13641 	class StringChangeEvent : ChangeEvent, Signal!string {
13642 		mixin SignalImpl
13643 	}
13644 
13645 +/
13646 class Event : ReflectableProperties {
13647 	/// Creates an event without populating any members and without sending it. See [dispatch]
13648 	this(string eventName, Widget emittedBy) {
13649 		this.eventName = eventName;
13650 		this.srcElement = emittedBy;
13651 	}
13652 
13653 
13654 	/// Implementations for the [ReflectableProperties] interface/
13655 	void getPropertiesList(scope void delegate(string name) sink) const {}
13656 	/// ditto
13657 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
13658 	/// ditto
13659 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
13660 		return SetPropertyResult.notPermitted;
13661 	}
13662 
13663 
13664 	/+
13665 	/++
13666 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
13667 
13668 		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.
13669 	+/
13670 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
13671 		if(value.length == 0) {
13672 			finalSink(memberName, `""`);
13673 			return;
13674 		}
13675 
13676 		char[1024] bufferBacking;
13677 		char[] buffer = bufferBacking;
13678 		int bufferPosition;
13679 
13680 		void sink(char ch) {
13681 			if(bufferPosition >= buffer.length)
13682 				buffer.length = buffer.length + 1024;
13683 			buffer[bufferPosition++] = ch;
13684 		}
13685 
13686 		sink('"');
13687 
13688 		foreach(ch; value) {
13689 			switch(ch) {
13690 				case '\\':
13691 					sink('\\'); sink('\\');
13692 				break;
13693 				case '"':
13694 					sink('\\'); sink('"');
13695 				break;
13696 				case '\n':
13697 					sink('\\'); sink('n');
13698 				break;
13699 				case '\r':
13700 					sink('\\'); sink('r');
13701 				break;
13702 				case '\t':
13703 					sink('\\'); sink('t');
13704 				break;
13705 				default:
13706 					sink(ch);
13707 			}
13708 		}
13709 
13710 		sink('"');
13711 
13712 		finalSink(memberName, buffer[0 .. bufferPosition]);
13713 	}
13714 	+/
13715 
13716 	/+
13717 	enum EventInitiator {
13718 		system,
13719 		minigui,
13720 		user
13721 	}
13722 
13723 	immutable EventInitiator; initiatedBy;
13724 	+/
13725 
13726 	/++
13727 		Events should generally follow the propagation model, but there's some exceptions
13728 		to that rule. If so, they should override this to return false. In that case, only
13729 		bubbling event handlers on the target itself and capturing event handlers on the containing
13730 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
13731 		capture -> target -> bubble process.)
13732 
13733 		History:
13734 			Added May 12, 2021
13735 	+/
13736 	bool propagates() const pure nothrow @nogc @safe {
13737 		return true;
13738 	}
13739 
13740 	/++
13741 		hints as to whether preventDefault will actually do anything. not entirely reliable.
13742 
13743 		History:
13744 			Added May 14, 2021
13745 	+/
13746 	bool cancelable() const pure nothrow @nogc @safe {
13747 		return true;
13748 	}
13749 
13750 	/++
13751 		You can mix this into child class to register some boilerplate. It includes the `EventString`
13752 		member, a constructor, and implementations of the dynamic get data interfaces.
13753 
13754 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
13755 
13756 
13757 		You can override the default EventString by simply providing your own in the form of
13758 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
13759 		which provides some namespace protection against conflicts in other libraries while still being fairly
13760 		easy to use.
13761 
13762 		If you provide your own constructor, it will override the default constructor provided here. A constructor
13763 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
13764 		first argument to your constructor.
13765 
13766 		History:
13767 			Added May 13, 2021.
13768 	+/
13769 	protected static mixin template Register() {
13770 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
13771 		this(Widget target) { super(EventString, target); }
13772 
13773 		mixin ReflectableProperties.RegisterGetters;
13774 	}
13775 
13776 	/++
13777 		This is the widget that emitted the event.
13778 
13779 
13780 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
13781 
13782 		History:
13783 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
13784 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
13785 			so I don't intend to remove these aliases.
13786 	+/
13787 	Widget source;
13788 	/// ditto
13789 	alias source target;
13790 	/// ditto
13791 	alias source srcElement;
13792 
13793 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
13794 
13795 	/// Prevents the default event handler (if there is one) from being called
13796 	void preventDefault() {
13797 		lastDefaultPrevented = true;
13798 		defaultPrevented = true;
13799 	}
13800 
13801 	/// Stops the event propagation immediately.
13802 	void stopPropagation() {
13803 		propagationStopped = true;
13804 	}
13805 
13806 	private bool defaultPrevented;
13807 	private bool propagationStopped;
13808 	private string eventName;
13809 
13810 	private bool isBubbling;
13811 
13812 	/// 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.
13813 	protected void adjustScrolling() { }
13814 	/// ditto
13815 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
13816 
13817 	/++
13818 		this sends it only to the target. If you want propagation, use dispatch() instead.
13819 
13820 		This should be made private!!!
13821 
13822 	+/
13823 	void sendDirectly() {
13824 		if(srcElement is null)
13825 			return;
13826 
13827 		// 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.
13828 
13829 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
13830 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
13831 
13832 		adjustScrolling();
13833 
13834 		if(auto e = target.parentWindow) {
13835 			if(auto handlers = "*" in e.capturingEventHandlers)
13836 			foreach(handler; *handlers)
13837 				if(handler) handler(e, this);
13838 			if(auto handlers = eventName in e.capturingEventHandlers)
13839 			foreach(handler; *handlers)
13840 				if(handler) handler(e, this);
13841 		}
13842 
13843 		auto e = srcElement;
13844 
13845 		if(auto handlers = eventName in e.bubblingEventHandlers)
13846 		foreach(handler; *handlers)
13847 			if(handler) handler(e, this);
13848 
13849 		if(auto handlers = "*" in e.bubblingEventHandlers)
13850 		foreach(handler; *handlers)
13851 			if(handler) handler(e, this);
13852 
13853 		// there's never a default for a catch-all event
13854 		if(!defaultPrevented)
13855 			if(eventName in e.defaultEventHandlers)
13856 				e.defaultEventHandlers[eventName](e, this);
13857 	}
13858 
13859 	/// this dispatches the element using the capture -> target -> bubble process
13860 	void dispatch() {
13861 		if(srcElement is null)
13862 			return;
13863 
13864 		if(!propagates) {
13865 			sendDirectly;
13866 			return;
13867 		}
13868 
13869 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
13870 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
13871 
13872 		adjustScrolling();
13873 		// first capture, then bubble
13874 
13875 		Widget[] chain;
13876 		Widget curr = srcElement;
13877 		while(curr) {
13878 			auto l = curr;
13879 			chain ~= l;
13880 			curr = curr.parent;
13881 		}
13882 
13883 		isBubbling = false;
13884 
13885 		foreach_reverse(e; chain) {
13886 			if(auto handlers = "*" in e.capturingEventHandlers)
13887 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13888 
13889 			if(propagationStopped)
13890 				break;
13891 
13892 			if(auto handlers = eventName in e.capturingEventHandlers)
13893 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13894 
13895 			// the default on capture should really be to always do nothing
13896 
13897 			//if(!defaultPrevented)
13898 			//	if(eventName in e.defaultEventHandlers)
13899 			//		e.defaultEventHandlers[eventName](e.element, this);
13900 
13901 			if(propagationStopped)
13902 				break;
13903 		}
13904 
13905 		int adjustX;
13906 		int adjustY;
13907 
13908 		isBubbling = true;
13909 		if(!propagationStopped)
13910 		foreach(e; chain) {
13911 			if(auto handlers = eventName in e.bubblingEventHandlers)
13912 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13913 
13914 			if(propagationStopped)
13915 				break;
13916 
13917 			if(auto handlers = "*" in e.bubblingEventHandlers)
13918 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
13919 
13920 			if(propagationStopped)
13921 				break;
13922 
13923 			if(e.encapsulatedChildren()) {
13924 				adjustClientCoordinates(adjustX, adjustY);
13925 				target = e;
13926 			} else {
13927 				adjustX += e.x;
13928 				adjustY += e.y;
13929 			}
13930 		}
13931 
13932 		if(!defaultPrevented)
13933 		foreach(e; chain) {
13934 			if(eventName in e.defaultEventHandlers)
13935 				e.defaultEventHandlers[eventName](e, this);
13936 		}
13937 	}
13938 
13939 
13940 	/* old compatibility things */
13941 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
13942 	final @property {
13943 		Key key() { return (cast(KeyEventBase) this).key; }
13944 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
13945 
13946 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
13947 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
13948 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
13949 	}
13950 
13951 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
13952 	final @property {
13953 		int clientX() { return (cast(MouseEventBase) this).clientX; }
13954 		int clientY() { return (cast(MouseEventBase) this).clientY; }
13955 
13956 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
13957 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
13958 
13959 		int button() { return (cast(MouseEventBase) this).button; }
13960 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
13961 	}
13962 
13963 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
13964 	final @property {
13965 		int state() {
13966 			if(auto meb = cast(MouseEventBase) this)
13967 				return meb.state;
13968 			if(auto keb = cast(KeyEventBase) this)
13969 				return keb.state;
13970 			assert(0);
13971 		}
13972 	}
13973 
13974 	deprecated("Use a CharEvent instead of Event in your handler going forward")
13975 	final @property {
13976 		dchar character() {
13977 			if(auto ce = cast(CharEvent) this)
13978 				return ce.character;
13979 			return dchar.init;
13980 		}
13981 	}
13982 
13983 	// for change events
13984 	@property {
13985 		///
13986 		int intValue() { return 0; }
13987 		///
13988 		string stringValue() { return null; }
13989 	}
13990 }
13991 
13992 /++
13993 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
13994 
13995 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
13996 	dynamic and custom events, but the static list helps ensure you get them right.
13997 
13998 	If this is declared, you can use [Widget.emit] to send the event.
13999 
14000 	All events work the same way though, following the capture->widget->bubble model described under [Event].
14001 
14002 	History:
14003 		Added May 4, 2021
14004 +/
14005 mixin template Emits(EventType) {
14006 	import arsd.minigui : EventString;
14007 	static if(is(EventType : Event) && !is(EventType == Event))
14008 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
14009 	else
14010 		static assert(0, "You can only emit subclasses of Event");
14011 }
14012 
14013 /// ditto
14014 mixin template Emits(string eventString) {
14015 	mixin("private Event[0] emits_" ~ eventString ~";");
14016 }
14017 
14018 /*
14019 class SignalEvent(string name) : Event {
14020 
14021 }
14022 */
14023 
14024 /++
14025 	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".
14026 
14027 
14028 	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.
14029 
14030 	History:
14031 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
14032 +/
14033 class CommandEvent : Event {
14034 	enum EventString = "command";
14035 	this(Widget source, string CommandString = EventString) {
14036 		super(CommandString, source);
14037 	}
14038 }
14039 
14040 /++
14041 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
14042 +/
14043 class CommandEventWithArgs(Args...) : CommandEvent {
14044 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
14045 	Args args;
14046 }
14047 
14048 /++
14049 	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.
14050 
14051 	See [CommandEvent] for more information.
14052 
14053 	Returns:
14054 		The [EventListener] you can use to remove the handler.
14055 +/
14056 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
14057 	return w.addEventListener(CommandString, (Event ev) {
14058 		if(ev.target is w)
14059 			return; // it does not consume its own commands!
14060 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
14061 			handler(cev.args);
14062 			ev.stopPropagation();
14063 		}
14064 	});
14065 }
14066 
14067 /++
14068 	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.
14069 +/
14070 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
14071 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
14072 	event.dispatch();
14073 }
14074 
14075 class ResizeEvent : Event {
14076 	enum EventString = "resize";
14077 
14078 	this(Widget target) { super(EventString, target); }
14079 
14080 	override bool propagates() const { return false; }
14081 }
14082 
14083 /++
14084 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
14085 
14086 	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.
14087 
14088 	History:
14089 		Added June 21, 2021 (dub v10.1)
14090 +/
14091 class ClosingEvent : Event {
14092 	enum EventString = "closing";
14093 
14094 	this(Widget target) { super(EventString, target); }
14095 
14096 	override bool propagates() const { return false; }
14097 	override bool cancelable() const { return true; }
14098 }
14099 
14100 /// ditto
14101 class ClosedEvent : Event {
14102 	enum EventString = "closed";
14103 
14104 	this(Widget target) { super(EventString, target); }
14105 
14106 	override bool propagates() const { return false; }
14107 	override bool cancelable() const { return false; }
14108 }
14109 
14110 ///
14111 class BlurEvent : Event {
14112 	enum EventString = "blur";
14113 
14114 	// FIXME: related target?
14115 	this(Widget target) { super(EventString, target); }
14116 
14117 	override bool propagates() const { return false; }
14118 }
14119 
14120 ///
14121 class FocusEvent : Event {
14122 	enum EventString = "focus";
14123 
14124 	// FIXME: related target?
14125 	this(Widget target) { super(EventString, target); }
14126 
14127 	override bool propagates() const { return false; }
14128 }
14129 
14130 /++
14131 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
14132 
14133 	History:
14134 		Added July 3, 2021
14135 +/
14136 class FocusInEvent : Event {
14137 	enum EventString = "focusin";
14138 
14139 	// FIXME: related target?
14140 	this(Widget target) { super(EventString, target); }
14141 
14142 	override bool cancelable() const { return false; }
14143 }
14144 
14145 /// ditto
14146 class FocusOutEvent : Event {
14147 	enum EventString = "focusout";
14148 
14149 	// FIXME: related target?
14150 	this(Widget target) { super(EventString, target); }
14151 
14152 	override bool cancelable() const { return false; }
14153 }
14154 
14155 ///
14156 class ScrollEvent : Event {
14157 	enum EventString = "scroll";
14158 	this(Widget target) { super(EventString, target); }
14159 
14160 	override bool cancelable() const { return false; }
14161 }
14162 
14163 /++
14164 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
14165 
14166 	History:
14167 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
14168 +/
14169 class CharEvent : Event {
14170 	enum EventString = "char";
14171 	this(Widget target, dchar ch) {
14172 		character = ch;
14173 		super(EventString, target);
14174 	}
14175 
14176 	immutable dchar character;
14177 }
14178 
14179 /++
14180 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
14181 +/
14182 abstract class ChangeEventBase : Event {
14183 	enum EventString = "change";
14184 	this(Widget target) {
14185 		super(EventString, target);
14186 	}
14187 
14188 	/+
14189 		// idk where or how exactly i want to do this.
14190 		// i might come back to it later.
14191 
14192 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
14193 	// this way the source doesn't get too confused (think of a nested scroll widget)
14194 	//
14195 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
14196 	// then you consume that command and change you scroll x position to whatever. then you do
14197 	// some kind of change event that is broadcast back to the children and any horizontal scroll
14198 	// listeners are now able to update, without having an explicit connection between them.
14199 	void broadcastToChildren(string fieldName) {
14200 
14201 	}
14202 	+/
14203 }
14204 
14205 /++
14206 	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.
14207 
14208 
14209 	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).
14210 
14211 	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);`
14212 
14213 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
14214 
14215 	History:
14216 		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.
14217 +/
14218 class ChangeEvent(T) : ChangeEventBase {
14219 	this(Widget target, T delegate() getNewValue) {
14220 		assert(getNewValue !is null);
14221 		this.getNewValue = getNewValue;
14222 		super(target);
14223 	}
14224 
14225 	private T delegate() getNewValue;
14226 
14227 	/++
14228 		Gets the new value that just changed.
14229 	+/
14230 	@property T value() {
14231 		return getNewValue();
14232 	}
14233 
14234 	/// compatibility method for old generic Events
14235 	static if(is(immutable T == immutable int))
14236 		override int intValue() { return value; }
14237 	/// ditto
14238 	static if(is(immutable T == immutable string))
14239 		override string stringValue() { return value; }
14240 }
14241 
14242 /++
14243 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
14244 
14245 
14246 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14247 
14248 	History:
14249 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14250 +/
14251 abstract class KeyEventBase : Event {
14252 	this(string name, Widget target) {
14253 		super(name, target);
14254 	}
14255 
14256 	// for key events
14257 	Key key; ///
14258 
14259 	KeyEvent originalKeyEvent;
14260 
14261 	/++
14262 		Indicates the current state of the given keyboard modifier keys.
14263 
14264 		History:
14265 			Added to events on April 15, 2020.
14266 	+/
14267 	bool ctrlKey;
14268 
14269 	/// ditto
14270 	bool altKey;
14271 
14272 	/// ditto
14273 	bool shiftKey;
14274 
14275 	/++
14276 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
14277 
14278 		See [arsd.simpledisplay.ModifierState] for other possible flags.
14279 	+/
14280 	int state;
14281 
14282 	mixin Register;
14283 }
14284 
14285 /++
14286 	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].
14287 
14288 
14289 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14290 
14291 	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.
14292 
14293 	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.
14294 
14295 	See_Also: [KeyUpEvent], [CharEvent]
14296 
14297 	History:
14298 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
14299 +/
14300 class KeyDownEvent : KeyEventBase {
14301 	enum EventString = "keydown";
14302 	this(Widget target) { super(EventString, target); }
14303 }
14304 
14305 /++
14306 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
14307 
14308 
14309 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14310 
14311 	See_Also: [KeyDownEvent], [CharEvent]
14312 
14313 	History:
14314 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
14315 +/
14316 class KeyUpEvent : KeyEventBase {
14317 	enum EventString = "keyup";
14318 	this(Widget target) { super(EventString, target); }
14319 }
14320 
14321 /++
14322 	Contains shared properties for various mouse events;
14323 
14324 
14325 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14326 
14327 	History:
14328 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14329 +/
14330 abstract class MouseEventBase : Event {
14331 	this(string name, Widget target) {
14332 		super(name, target);
14333 	}
14334 
14335 	// for mouse events
14336 	int clientX; /// The mouse event location relative to the target widget
14337 	int clientY; /// ditto
14338 
14339 	int viewportX; /// The mouse event location relative to the window origin
14340 	int viewportY; /// ditto
14341 
14342 	int button; /// See: [MouseEvent.button]
14343 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
14344 
14345 	/++
14346 		Indicates the current state of the given keyboard modifier keys.
14347 
14348 		History:
14349 			Added to mouse events on September 28, 2010.
14350 	+/
14351 	bool ctrlKey;
14352 
14353 	/// ditto
14354 	bool altKey;
14355 
14356 	/// ditto
14357 	bool shiftKey;
14358 
14359 
14360 
14361 	int state; ///
14362 
14363 	/++
14364 		for consistent names with key event.
14365 
14366 		History:
14367 			Added September 28, 2021 (dub v10.3)
14368 	+/
14369 	alias modifierState = state;
14370 
14371 	/++
14372 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
14373 
14374 		History:
14375 			Added May 15, 2021
14376 	+/
14377 	bool isMouseWheel() {
14378 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
14379 	}
14380 
14381 	// private
14382 	override void adjustClientCoordinates(int deltaX, int deltaY) {
14383 		clientX += deltaX;
14384 		clientY += deltaY;
14385 	}
14386 
14387 	override void adjustScrolling() {
14388 	version(custom_widgets) { // TEMP
14389 		viewportX = clientX;
14390 		viewportY = clientY;
14391 		if(auto se = cast(ScrollableWidget) srcElement) {
14392 			clientX += se.scrollOrigin.x;
14393 			clientY += se.scrollOrigin.y;
14394 		} else if(auto se = cast(ScrollableContainerWidget) srcElement) {
14395 			//clientX += se.scrollX_;
14396 			//clientY += se.scrollY_;
14397 		}
14398 	}
14399 	}
14400 
14401 	mixin Register;
14402 }
14403 
14404 /++
14405 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
14406 
14407 
14408 	$(WARNING
14409 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
14410 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
14411 		behavior.
14412 	)
14413 
14414 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
14415 
14416 	[MouseUpEvent] is sent when the user releases a mouse button.
14417 
14418 	[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.)
14419 
14420 	[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.
14421 
14422 	[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.
14423 
14424 	[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.
14425 
14426 	[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.
14427 
14428 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
14429 
14430 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
14431 
14432 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14433 
14434 	Rationale:
14435 
14436 		If you only want to do drag, mousedown/up works just fine being consistently sent.
14437 
14438 		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).
14439 
14440 		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.
14441 
14442 	History:
14443 		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.
14444 +/
14445 class MouseUpEvent : MouseEventBase {
14446 	enum EventString = "mouseup"; ///
14447 	this(Widget target) { super(EventString, target); }
14448 }
14449 /// ditto
14450 class MouseDownEvent : MouseEventBase {
14451 	enum EventString = "mousedown"; ///
14452 	this(Widget target) { super(EventString, target); }
14453 }
14454 /// ditto
14455 class MouseMoveEvent : MouseEventBase {
14456 	enum EventString = "mousemove"; ///
14457 	this(Widget target) { super(EventString, target); }
14458 }
14459 /// ditto
14460 class ClickEvent : MouseEventBase {
14461 	enum EventString = "click"; ///
14462 	this(Widget target) { super(EventString, target); }
14463 }
14464 /// ditto
14465 class DoubleClickEvent : MouseEventBase {
14466 	enum EventString = "dblclick"; ///
14467 	this(Widget target) { super(EventString, target); }
14468 }
14469 /// ditto
14470 class MouseOverEvent : Event {
14471 	enum EventString = "mouseover"; ///
14472 	this(Widget target) { super(EventString, target); }
14473 }
14474 /// ditto
14475 class MouseOutEvent : Event {
14476 	enum EventString = "mouseout"; ///
14477 	this(Widget target) { super(EventString, target); }
14478 }
14479 /// ditto
14480 class MouseEnterEvent : Event {
14481 	enum EventString = "mouseenter"; ///
14482 	this(Widget target) { super(EventString, target); }
14483 
14484 	override bool propagates() const { return false; }
14485 }
14486 /// ditto
14487 class MouseLeaveEvent : Event {
14488 	enum EventString = "mouseleave"; ///
14489 	this(Widget target) { super(EventString, target); }
14490 
14491 	override bool propagates() const { return false; }
14492 }
14493 
14494 private bool isAParentOf(Widget a, Widget b) {
14495 	if(a is null || b is null)
14496 		return false;
14497 
14498 	while(b !is null) {
14499 		if(a is b)
14500 			return true;
14501 		b = b.parent;
14502 	}
14503 
14504 	return false;
14505 }
14506 
14507 private struct WidgetAtPointResponse {
14508 	Widget widget;
14509 
14510 	// x, y relative to the widget in the response.
14511 	int x;
14512 	int y;
14513 }
14514 
14515 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
14516 	assert(starting !is null);
14517 
14518 	starting.addScrollPosition(x, y);
14519 
14520 	auto child = starting.getChildAtPosition(x, y);
14521 	while(child) {
14522 		if(child.hidden)
14523 			continue;
14524 		starting = child;
14525 		x -= child.x;
14526 		y -= child.y;
14527 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
14528 		child = r.widget;
14529 		if(child is starting)
14530 			break;
14531 	}
14532 	return WidgetAtPointResponse(starting, x, y);
14533 }
14534 
14535 version(win32_widgets) {
14536 private:
14537 	import core.sys.windows.commctrl;
14538 
14539 	pragma(lib, "comctl32");
14540 	shared static this() {
14541 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
14542 		INITCOMMONCONTROLSEX ic;
14543 		ic.dwSize = cast(DWORD) ic.sizeof;
14544 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
14545 		if(!InitCommonControlsEx(&ic)) {
14546 			//writeln("ICC failed");
14547 		}
14548 	}
14549 
14550 
14551 	// everything from here is just win32 headers copy pasta
14552 private:
14553 extern(Windows):
14554 
14555 	alias HANDLE HMENU;
14556 	HMENU CreateMenu();
14557 	bool SetMenu(HWND, HMENU);
14558 	HMENU CreatePopupMenu();
14559 	enum MF_POPUP = 0x10;
14560 	enum MF_STRING = 0;
14561 
14562 
14563 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
14564 	struct INITCOMMONCONTROLSEX {
14565 		DWORD dwSize;
14566 		DWORD dwICC;
14567 	}
14568 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
14569 enum {
14570         IDB_STD_SMALL_COLOR,
14571         IDB_STD_LARGE_COLOR,
14572         IDB_VIEW_SMALL_COLOR = 4,
14573         IDB_VIEW_LARGE_COLOR = 5
14574 }
14575 enum {
14576         STD_CUT,
14577         STD_COPY,
14578         STD_PASTE,
14579         STD_UNDO,
14580         STD_REDOW,
14581         STD_DELETE,
14582         STD_FILENEW,
14583         STD_FILEOPEN,
14584         STD_FILESAVE,
14585         STD_PRINTPRE,
14586         STD_PROPERTIES,
14587         STD_HELP,
14588         STD_FIND,
14589         STD_REPLACE,
14590         STD_PRINT // = 14
14591 }
14592 
14593 alias HANDLE HIMAGELIST;
14594 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
14595 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
14596         BOOL ImageList_Destroy(HIMAGELIST);
14597 
14598 uint MAKELONG(ushort a, ushort b) {
14599         return cast(uint) ((b << 16) | a);
14600 }
14601 
14602 
14603 struct TBBUTTON {
14604 	int   iBitmap;
14605 	int   idCommand;
14606 	BYTE  fsState;
14607 	BYTE  fsStyle;
14608 	version(Win64)
14609 	BYTE[6] bReserved;
14610 	else
14611 	BYTE[2]  bReserved;
14612 	DWORD dwData;
14613 	INT_PTR   iString;
14614 }
14615 
14616 	enum {
14617 		TB_ADDBUTTONSA   = WM_USER + 20,
14618 		TB_INSERTBUTTONA = WM_USER + 21,
14619 		TB_GETIDEALSIZE = WM_USER + 99,
14620 	}
14621 
14622 struct SIZE {
14623 	LONG cx;
14624 	LONG cy;
14625 }
14626 
14627 
14628 enum {
14629 	TBSTATE_CHECKED       = 1,
14630 	TBSTATE_PRESSED       = 2,
14631 	TBSTATE_ENABLED       = 4,
14632 	TBSTATE_HIDDEN        = 8,
14633 	TBSTATE_INDETERMINATE = 16,
14634 	TBSTATE_WRAP          = 32
14635 }
14636 
14637 
14638 
14639 enum {
14640 	ILC_COLOR    = 0,
14641 	ILC_COLOR4   = 4,
14642 	ILC_COLOR8   = 8,
14643 	ILC_COLOR16  = 16,
14644 	ILC_COLOR24  = 24,
14645 	ILC_COLOR32  = 32,
14646 	ILC_COLORDDB = 254,
14647 	ILC_MASK     = 1,
14648 	ILC_PALETTE  = 2048
14649 }
14650 
14651 
14652 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
14653 
14654 
14655 enum {
14656 	TB_ENABLEBUTTON          = WM_USER + 1,
14657 	TB_CHECKBUTTON,
14658 	TB_PRESSBUTTON,
14659 	TB_HIDEBUTTON,
14660 	TB_INDETERMINATE, //     = WM_USER + 5,
14661 	TB_ISBUTTONENABLED       = WM_USER + 9,
14662 	TB_ISBUTTONCHECKED,
14663 	TB_ISBUTTONPRESSED,
14664 	TB_ISBUTTONHIDDEN,
14665 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
14666 	TB_SETSTATE              = WM_USER + 17,
14667 	TB_GETSTATE              = WM_USER + 18,
14668 	TB_ADDBITMAP             = WM_USER + 19,
14669 	TB_DELETEBUTTON          = WM_USER + 22,
14670 	TB_GETBUTTON,
14671 	TB_BUTTONCOUNT,
14672 	TB_COMMANDTOINDEX,
14673 	TB_SAVERESTOREA,
14674 	TB_CUSTOMIZE,
14675 	TB_ADDSTRINGA,
14676 	TB_GETITEMRECT,
14677 	TB_BUTTONSTRUCTSIZE,
14678 	TB_SETBUTTONSIZE,
14679 	TB_SETBITMAPSIZE,
14680 	TB_AUTOSIZE, //          = WM_USER + 33,
14681 	TB_GETTOOLTIPS           = WM_USER + 35,
14682 	TB_SETTOOLTIPS           = WM_USER + 36,
14683 	TB_SETPARENT             = WM_USER + 37,
14684 	TB_SETROWS               = WM_USER + 39,
14685 	TB_GETROWS,
14686 	TB_GETBITMAPFLAGS,
14687 	TB_SETCMDID,
14688 	TB_CHANGEBITMAP,
14689 	TB_GETBITMAP,
14690 	TB_GETBUTTONTEXTA,
14691 	TB_REPLACEBITMAP, //     = WM_USER + 46,
14692 	TB_GETBUTTONSIZE         = WM_USER + 58,
14693 	TB_SETBUTTONWIDTH        = WM_USER + 59,
14694 	TB_GETBUTTONTEXTW        = WM_USER + 75,
14695 	TB_SAVERESTOREW          = WM_USER + 76,
14696 	TB_ADDSTRINGW            = WM_USER + 77,
14697 }
14698 
14699 extern(Windows)
14700 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
14701 
14702 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
14703 
14704 
14705 	enum {
14706 		TB_SETINDENT = WM_USER + 47,
14707 		TB_SETIMAGELIST,
14708 		TB_GETIMAGELIST,
14709 		TB_LOADIMAGES,
14710 		TB_GETRECT,
14711 		TB_SETHOTIMAGELIST,
14712 		TB_GETHOTIMAGELIST,
14713 		TB_SETDISABLEDIMAGELIST,
14714 		TB_GETDISABLEDIMAGELIST,
14715 		TB_SETSTYLE,
14716 		TB_GETSTYLE,
14717 		//TB_GETBUTTONSIZE,
14718 		//TB_SETBUTTONWIDTH,
14719 		TB_SETMAXTEXTROWS,
14720 		TB_GETTEXTROWS // = WM_USER + 61
14721 	}
14722 
14723 enum {
14724 	CCM_FIRST            = 0x2000,
14725 	CCM_LAST             = CCM_FIRST + 0x200,
14726 	CCM_SETBKCOLOR       = 8193,
14727 	CCM_SETCOLORSCHEME   = 8194,
14728 	CCM_GETCOLORSCHEME   = 8195,
14729 	CCM_GETDROPTARGET    = 8196,
14730 	CCM_SETUNICODEFORMAT = 8197,
14731 	CCM_GETUNICODEFORMAT = 8198,
14732 	CCM_SETVERSION       = 0x2007,
14733 	CCM_GETVERSION       = 0x2008,
14734 	CCM_SETNOTIFYWINDOW  = 0x2009
14735 }
14736 
14737 
14738 enum {
14739 	PBM_SETRANGE     = WM_USER + 1,
14740 	PBM_SETPOS,
14741 	PBM_DELTAPOS,
14742 	PBM_SETSTEP,
14743 	PBM_STEPIT,   // = WM_USER + 5
14744 	PBM_SETRANGE32   = 1030,
14745 	PBM_GETRANGE,
14746 	PBM_GETPOS,
14747 	PBM_SETBARCOLOR, // = 1033
14748 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
14749 }
14750 
14751 enum {
14752 	PBS_SMOOTH   = 1,
14753 	PBS_VERTICAL = 4
14754 }
14755 
14756 enum {
14757         ICC_LISTVIEW_CLASSES = 1,
14758         ICC_TREEVIEW_CLASSES = 2,
14759         ICC_BAR_CLASSES      = 4,
14760         ICC_TAB_CLASSES      = 8,
14761         ICC_UPDOWN_CLASS     = 16,
14762         ICC_PROGRESS_CLASS   = 32,
14763         ICC_HOTKEY_CLASS     = 64,
14764         ICC_ANIMATE_CLASS    = 128,
14765         ICC_WIN95_CLASSES    = 255,
14766         ICC_DATE_CLASSES     = 256,
14767         ICC_USEREX_CLASSES   = 512,
14768         ICC_COOL_CLASSES     = 1024,
14769 	ICC_STANDARD_CLASSES = 0x00004000,
14770 }
14771 
14772 	enum WM_USER = 1024;
14773 }
14774 
14775 version(win32_widgets)
14776 	pragma(lib, "comdlg32");
14777 
14778 
14779 ///
14780 enum GenericIcons : ushort {
14781 	None, ///
14782 	// these happen to match the win32 std icons numerically if you just subtract one from the value
14783 	Cut, ///
14784 	Copy, ///
14785 	Paste, ///
14786 	Undo, ///
14787 	Redo, ///
14788 	Delete, ///
14789 	New, ///
14790 	Open, ///
14791 	Save, ///
14792 	PrintPreview, ///
14793 	Properties, ///
14794 	Help, ///
14795 	Find, ///
14796 	Replace, ///
14797 	Print, ///
14798 }
14799 
14800 enum FileDialogType {
14801 	Automatic,
14802 	Open,
14803 	Save
14804 }
14805 string previousFileReferenced;
14806 
14807 /++
14808 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
14809 
14810 	Params:
14811 		storage = an alias to a `static string` variable that stores the last file referenced. It will
14812 		use this to pre-fill the dialog with a suggestion.
14813 
14814 		Please note that it MUST be `static` or you will get compile errors.
14815 
14816 		filters = the filters param to [getFileName]
14817 
14818 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
14819 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
14820 		a save dialog box. Otherwise, it will show an open dialog box.
14821 +/
14822 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
14823 	string name;
14824 	alias name this;
14825 }
14826 
14827 /++
14828 	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.
14829 
14830 	History:
14831 		onCancel was added November 6, 2021.
14832 
14833 		The dialog itself on Linux was modified on December 2, 2021 to include
14834 		a directory picker in addition to the command line completion view.
14835 
14836 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
14837 	Future_directions:
14838 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
14839 		at least on Linux, maybe on Windows too.
14840 +/
14841 void getOpenFileName(
14842 	void delegate(string) onOK,
14843 	string prefilledName = null,
14844 	string[] filters = null,
14845 	void delegate() onCancel = null,
14846 	string initialDirectory = null,
14847 )
14848 {
14849 	return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory);
14850 }
14851 
14852 /// ditto
14853 void getSaveFileName(
14854 	void delegate(string) onOK,
14855 	string prefilledName = null,
14856 	string[] filters = null,
14857 	void delegate() onCancel = null,
14858 	string initialDirectory = null,
14859 )
14860 {
14861 	return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory);
14862 }
14863 
14864 void getFileName(
14865 	bool openOrSave,
14866 	void delegate(string) onOK,
14867 	string prefilledName = null,
14868 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
14869 	void delegate() onCancel = null,
14870 	string initialDirectory = null,
14871 )
14872 {
14873 
14874 	version(win32_widgets) {
14875 		import core.sys.windows.commdlg;
14876 	/*
14877 	Ofn.lStructSize = sizeof(OPENFILENAME);
14878 	Ofn.hwndOwner = hWnd;
14879 	Ofn.lpstrFilter = szFilter;
14880 	Ofn.lpstrFile= szFile;
14881 	Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
14882 	Ofn.lpstrFileTitle = szFileTitle;
14883 	Ofn.nMaxFileTitle = sizeof(szFileTitle);
14884 	Ofn.lpstrInitialDir = (LPSTR)NULL;
14885 	Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
14886 	Ofn.lpstrTitle = szTitle;
14887 	 */
14888 
14889 
14890 		wchar[1024] file = 0;
14891 		wchar[1024] filterBuffer = 0;
14892 		makeWindowsString(prefilledName, file[]);
14893 		OPENFILENAME ofn;
14894 		ofn.lStructSize = ofn.sizeof;
14895 		if(filters.length) {
14896 			string filter;
14897 			foreach(i, f; filters) {
14898 				filter ~= f;
14899 				filter ~= "\0";
14900 			}
14901 			filter ~= "\0";
14902 			ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
14903 		}
14904 		ofn.lpstrFile = file.ptr;
14905 		ofn.nMaxFile = file.length;
14906 
14907 		wchar[1024] initialDir = 0;
14908 		if(initialDirectory !is null) {
14909 			makeWindowsString(initialDirectory, initialDir[]);
14910 			ofn.lpstrInitialDir = file.ptr;
14911 		}
14912 
14913 		if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
14914 		{
14915 			string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
14916 			if(okString.length && okString[$-1] == '\0')
14917 				okString = okString[0..$-1];
14918 			onOK(okString);
14919 		} else {
14920 			if(onCancel)
14921 				onCancel();
14922 		}
14923 	} else version(custom_widgets) {
14924 		if(filters.length == 0)
14925 			filters = ["All Files\0*.*"];
14926 		auto picker = new FilePicker(prefilledName, filters, initialDirectory);
14927 		picker.onOK = onOK;
14928 		picker.onCancel = onCancel;
14929 		picker.show();
14930 	}
14931 }
14932 
14933 version(custom_widgets)
14934 private
14935 class FilePicker : Dialog {
14936 	void delegate(string) onOK;
14937 	void delegate() onCancel;
14938 	LineEdit lineEdit;
14939 
14940 	// returns common prefix
14941 	string loadFiles(string cwd, string[] filters...) {
14942 		string[] files;
14943 		string[] dirs;
14944 
14945 		string commonPrefix;
14946 
14947 		getFiles(cwd, (string name, bool isDirectory) {
14948 			if(name == ".")
14949 				return; // skip this as unnecessary
14950 			if(isDirectory)
14951 				dirs ~= name;
14952 			else {
14953 				foreach(filter; filters)
14954 				if(
14955 					filter.length <= 1 ||
14956 					filter == "*.*" ||
14957 					(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
14958 					(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
14959 				)
14960 				{
14961 					files ~= name;
14962 
14963 					if(filter.length > 0 && filter[$-1] == '*') {
14964 						if(commonPrefix is null) {
14965 							commonPrefix = name;
14966 						} else {
14967 							foreach(idx, char i; name) {
14968 								if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
14969 									commonPrefix = commonPrefix[0 .. idx];
14970 									break;
14971 								}
14972 							}
14973 						}
14974 					}
14975 
14976 					break;
14977 				}
14978 			}
14979 		});
14980 
14981 		extern(C) static int comparator(scope const void* a, scope const void* b) {
14982 			auto sa = *cast(string*) a;
14983 			auto sb = *cast(string*) b;
14984 
14985 			for(int i = 0; i < sa.length; i++) {
14986 				if(i == sb.length)
14987 					return 1;
14988 				return sa[i] - sb[i];
14989 			}
14990 
14991 			return 0;
14992 		}
14993 
14994 		nonPhobosSort(files, &comparator);
14995 		nonPhobosSort(dirs, &comparator);
14996 
14997 		listWidget.clear();
14998 		dirWidget.clear();
14999 		foreach(name; dirs)
15000 			dirWidget.addOption(name);
15001 		foreach(name; files)
15002 			listWidget.addOption(name);
15003 
15004 		return commonPrefix;
15005 	}
15006 
15007 	ListWidget listWidget;
15008 	ListWidget dirWidget;
15009 
15010 	string currentDirectory;
15011 	string[] processedFilters;
15012 
15013 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"]
15014 	this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) {
15015 		super(300, 200, "Choose File..."); // owner);
15016 
15017 		foreach(filter; filters) {
15018 			while(filter.length && filter[0] != 0) {
15019 				filter = filter[1 .. $];
15020 			}
15021 			if(filter.length)
15022 				filter = filter[1 .. $]; // trim off the 0
15023 
15024 			while(filter.length) {
15025 				int idx = 0;
15026 				while(idx < filter.length && filter[idx] != ';') {
15027 					idx++;
15028 				}
15029 
15030 				processedFilters ~= filter[0 .. idx];
15031 				if(idx < filter.length)
15032 					idx++; // skip the ;
15033 				filter = filter[idx .. $];
15034 			}
15035 		}
15036 
15037 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
15038 
15039 		{
15040 			auto hl = new HorizontalLayout(this);
15041 			dirWidget = new ListWidget(hl);
15042 			listWidget = new ListWidget(hl);
15043 
15044 			// double click events normally trigger something else but
15045 			// here user might be clicking kinda fast and we'd rather just
15046 			// keep it
15047 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
15048 				auto ce = new ChangeEvent!void(dirWidget, () {});
15049 				ce.dispatch();
15050 			});
15051 
15052 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
15053 				string v;
15054 				foreach(o; dirWidget.options)
15055 					if(o.selected) {
15056 						v = o.label;
15057 						break;
15058 					}
15059 				if(v.length) {
15060 					currentDirectory ~= "/" ~ v;
15061 					loadFiles(currentDirectory, processedFilters);
15062 				}
15063 			});
15064 
15065 			// double click here, on the other hand, selects the file
15066 			// and moves on
15067 			listWidget.addEventListener((scope DoubleClickEvent dev) {
15068 				OK();
15069 			});
15070 		}
15071 
15072 		lineEdit = new LineEdit(this);
15073 		lineEdit.focus();
15074 		lineEdit.addEventListener(delegate(CharEvent event) {
15075 			if(event.character == '\t' || event.character == '\n')
15076 				event.preventDefault();
15077 		});
15078 
15079 		listWidget.addEventListener(EventType.change, () {
15080 			foreach(o; listWidget.options)
15081 				if(o.selected)
15082 					lineEdit.content = o.label;
15083 		});
15084 
15085 		loadFiles(currentDirectory, processedFilters);
15086 
15087 		lineEdit.addEventListener((KeyDownEvent event) {
15088 			if(event.key == Key.Tab) {
15089 
15090 				auto current = lineEdit.content;
15091 				if(current.length >= 2 && current[0 ..2] == "./")
15092 					current = current[2 .. $];
15093 
15094 				auto commonPrefix = loadFiles(".", current ~ "*");
15095 
15096 				if(commonPrefix.length)
15097 					lineEdit.content = commonPrefix;
15098 
15099 				// FIXME: if that is a directory, add the slash? or even go inside?
15100 
15101 				event.preventDefault();
15102 			}
15103 		});
15104 
15105 		lineEdit.content = prefilledName;
15106 
15107 		auto hl = new HorizontalLayout(60, this);
15108 		auto cancelButton = new Button("Cancel", hl);
15109 		auto okButton = new Button("OK", hl);
15110 
15111 		cancelButton.addEventListener(EventType.triggered, &Cancel);
15112 		okButton.addEventListener(EventType.triggered, &OK);
15113 
15114 		this.addEventListener((KeyDownEvent event) {
15115 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
15116 				event.preventDefault();
15117 				OK();
15118 			}
15119 			if(event.key == Key.Escape)
15120 				Cancel();
15121 		});
15122 
15123 	}
15124 
15125 	override void OK() {
15126 		if(lineEdit.content.length) {
15127 			string accepted;
15128 			auto c = lineEdit.content;
15129 			if(c.length && c[0] == '/')
15130 				accepted = c;
15131 			else
15132 				accepted = currentDirectory ~ "/" ~ lineEdit.content;
15133 
15134 			if(isDir(accepted)) {
15135 				// FIXME: would be kinda nice to support ~ and collapse these paths too
15136 				// FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later.
15137 				currentDirectory = accepted;
15138 				loadFiles(currentDirectory, processedFilters);
15139 				lineEdit.content = "";
15140 				return;
15141 			}
15142 
15143 			if(onOK)
15144 				onOK(accepted);
15145 		}
15146 		close();
15147 	}
15148 
15149 	override void Cancel() {
15150 		if(onCancel)
15151 			onCancel();
15152 		close();
15153 	}
15154 }
15155 
15156 private bool isDir(string name) {
15157 	version(Windows) {
15158 		auto ws = WCharzBuffer(name);
15159 		auto ret = GetFileAttributesW(ws.ptr);
15160 		if(ret == INVALID_FILE_ATTRIBUTES)
15161 			return false;
15162 		return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0;
15163 	} else version(Posix) {
15164 		import core.sys.posix.sys.stat;
15165 		stat_t buf;
15166 		auto ret = stat((name ~ '\0').ptr, &buf);
15167 		if(ret == -1)
15168 			return false; // I could probably check more specific errors tbh
15169 		return (buf.st_mode & S_IFMT) == S_IFDIR;
15170 	} else return false;
15171 }
15172 
15173 /*
15174 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
15175 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
15176 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
15177 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
15178 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
15179 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
15180 http://www.sbin.org/doc/Xlib/chapt_03.html
15181 
15182 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
15183 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
15184 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
15185 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
15186 */
15187 
15188 
15189 // These are all for setMenuAndToolbarFromAnnotatedCode
15190 /// This item in the menu will be preceded by a separator line
15191 /// Group: generating_from_code
15192 struct separator {}
15193 deprecated("It was misspelled, use separator instead") alias seperator = separator;
15194 /// Program-wide keyboard shortcut to trigger the action
15195 /// Group: generating_from_code
15196 struct accelerator { string keyString; }
15197 /// tells which menu the action will be on
15198 /// Group: generating_from_code
15199 struct menu { string name; }
15200 /// Describes which toolbar section the action appears on
15201 /// Group: generating_from_code
15202 struct toolbar { string groupName; }
15203 ///
15204 /// Group: generating_from_code
15205 struct icon { ushort id; }
15206 ///
15207 /// Group: generating_from_code
15208 struct label { string label; }
15209 ///
15210 /// Group: generating_from_code
15211 struct hotkey { dchar ch; }
15212 ///
15213 /// Group: generating_from_code
15214 struct tip { string tip; }
15215 
15216 
15217 /++
15218 	Observes and allows inspection of an object via automatic gui
15219 +/
15220 /// Group: generating_from_code
15221 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
15222 	return new ObjectInspectionWindowImpl!(T)(t);
15223 }
15224 
15225 class ObjectInspectionWindow : Window {
15226 	this(int a, int b, string c) {
15227 		super(a, b, c);
15228 	}
15229 
15230 	abstract void readUpdatesFromObject();
15231 }
15232 
15233 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
15234 	T t;
15235 	this(T t) {
15236 		this.t = t;
15237 
15238 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
15239 
15240 		foreach(memberName; __traits(derivedMembers, T)) {{
15241 			alias member = I!(__traits(getMember, t, memberName))[0];
15242 			alias type = typeof(member);
15243 			static if(is(type == int)) {
15244 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
15245 				//le.addEventListener("char", (Event ev) {
15246 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
15247 						//ev.preventDefault();
15248 				//});
15249 				le.addEventListener(EventType.change, (Event ev) {
15250 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
15251 				});
15252 
15253 				updateMemberDelegates[memberName] = () {
15254 					le.content = toInternal!string(__traits(getMember, t, memberName));
15255 				};
15256 			}
15257 		}}
15258 	}
15259 
15260 	void delegate()[string] updateMemberDelegates;
15261 
15262 	override void readUpdatesFromObject() {
15263 		foreach(k, v; updateMemberDelegates)
15264 			v();
15265 	}
15266 }
15267 
15268 /++
15269 	Creates a dialog based on a data structure.
15270 
15271 	---
15272 	dialog((YourStructure value) {
15273 		// the user filled in the struct and clicked OK,
15274 		// you can check the members now
15275 	});
15276 	---
15277 
15278 	Params:
15279 		initialData = the initial value to show in the dialog. It will not modify this unless
15280 		it is a class then it might, no promises.
15281 
15282 	History:
15283 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
15284 +/
15285 /// Group: generating_from_code
15286 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15287 	dialog(T.init, onOK, onCancel, title);
15288 }
15289 /// ditto
15290 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15291 	auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title);
15292 	dg.show();
15293 }
15294 
15295 private static template I(T...) { alias I = T; }
15296 
15297 
15298 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
15299 	if(name == "id")
15300 		return allLowerCase ? name : "ID";
15301 
15302 	char[160] buffer;
15303 	int bufferIndex = 0;
15304 	bool shouldCap = true;
15305 	bool shouldSpace;
15306 	bool lastWasCap;
15307 	foreach(idx, char ch; name) {
15308 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15309 
15310 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
15311 			if(lastWasCap) {
15312 				// two caps in a row, don't change. Prolly acronym.
15313 			} else {
15314 				if(idx)
15315 					shouldSpace = true; // new word, add space
15316 			}
15317 
15318 			lastWasCap = true;
15319 		} else {
15320 			lastWasCap = false;
15321 		}
15322 
15323 		if(shouldSpace) {
15324 			buffer[bufferIndex++] = space;
15325 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15326 			shouldSpace = false;
15327 		}
15328 		if(shouldCap) {
15329 			if(ch >= 'a' && ch <= 'z')
15330 				ch -= 32;
15331 			shouldCap = false;
15332 		}
15333 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
15334 			ch += 32;
15335 		buffer[bufferIndex++] = ch;
15336 	}
15337 	return buffer[0 .. bufferIndex].idup;
15338 }
15339 
15340 /++
15341 	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.
15342 +/
15343 class AutomaticDialog(T) : Dialog {
15344 	T t;
15345 
15346 	void delegate(T) onOK;
15347 	void delegate() onCancel;
15348 
15349 	override int paddingTop() { return defaultLineHeight; }
15350 	override int paddingBottom() { return defaultLineHeight; }
15351 	override int paddingRight() { return defaultLineHeight; }
15352 	override int paddingLeft() { return defaultLineHeight; }
15353 
15354 	this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
15355 		assert(onOK !is null);
15356 
15357 		t = initialData;
15358 
15359 		static if(is(T == class)) {
15360 			if(t is null)
15361 				t = new T();
15362 		}
15363 		this.onOK = onOK;
15364 		this.onCancel = onCancel;
15365 		super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
15366 
15367 		static if(is(T == class))
15368 			this.addDataControllerWidget(t);
15369 		else
15370 			this.addDataControllerWidget(&t);
15371 
15372 		auto hl = new HorizontalLayout(this);
15373 		auto stretch = new HorizontalSpacer(hl); // to right align
15374 		auto ok = new CommandButton("OK", hl);
15375 		auto cancel = new CommandButton("Cancel", hl);
15376 		ok.addEventListener(EventType.triggered, &OK);
15377 		cancel.addEventListener(EventType.triggered, &Cancel);
15378 
15379 		this.addEventListener((KeyDownEvent ev) {
15380 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
15381 				ok.focus();
15382 				OK();
15383 				ev.preventDefault();
15384 			}
15385 			if(ev.key == Key.Escape) {
15386 				Cancel();
15387 				ev.preventDefault();
15388 			}
15389 		});
15390 
15391 		this.addEventListener((scope ClosedEvent ce) {
15392 			if(onCancel)
15393 				onCancel();
15394 		});
15395 
15396 		//this.children[0].focus();
15397 	}
15398 
15399 	override void OK() {
15400 		onOK(t);
15401 		close();
15402 	}
15403 
15404 	override void Cancel() {
15405 		if(onCancel)
15406 			onCancel();
15407 		close();
15408 	}
15409 }
15410 
15411 private template baseClassCount(Class) {
15412 	private int helper() {
15413 		int count = 0;
15414 		static if(is(Class bases == super)) {
15415 			foreach(base; bases)
15416 				static if(is(base == class))
15417 					count += 1 + baseClassCount!base;
15418 		}
15419 		return count;
15420 	}
15421 
15422 	enum int baseClassCount = helper();
15423 }
15424 
15425 private long stringToLong(string s) {
15426 	long ret;
15427 	if(s.length == 0)
15428 		return ret;
15429 	bool negative = s[0] == '-';
15430 	if(negative)
15431 		s = s[1 .. $];
15432 	foreach(ch; s) {
15433 		if(ch >= '0' && ch <= '9') {
15434 			ret *= 10;
15435 			ret += ch - '0';
15436 		}
15437 	}
15438 	if(negative)
15439 		ret = -ret;
15440 	return ret;
15441 }
15442 
15443 
15444 interface ReflectableProperties {
15445 	/++
15446 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
15447 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
15448 		json in the current implementation.
15449 
15450 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
15451 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
15452 		as of the June 2, 2021 release.
15453 
15454 		History:
15455 			Added June 2, 2021.
15456 
15457 		See_Also: [getPropertyAsString], [setPropertyFromString]
15458 	+/
15459 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
15460 	/++
15461 		Requests a property to be delivered to you as a string, through your `sink` delegate.
15462 
15463 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
15464 		be interpreted as json, otherwise, it is just a plain string.
15465 
15466 		The sink should always be called exactly once for each call (it is basically a return value, but it might
15467 		use a local buffer it maintains instead of allocating a return value).
15468 
15469 		History:
15470 			Added June 2, 2021.
15471 
15472 		See_Also: [getPropertiesList], [setPropertyFromString]
15473 	+/
15474 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
15475 	/++
15476 		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.
15477 
15478 		History:
15479 			Added June 2, 2021.
15480 
15481 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
15482 	+/
15483 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
15484 
15485 	/// [setPropertyFromString] possible return values
15486 	enum SetPropertyResult {
15487 		success = 0, /// the property has been successfully set to the request value
15488 		notPermitted = -1, /// the property exists but it cannot be changed at this time
15489 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
15490 		noSuchProperty = -3, /// there is no property by that name
15491 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
15492 		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)
15493 	}
15494 
15495 	/++
15496 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
15497 
15498 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
15499 
15500 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
15501 		rarely need to use these building blocks directly.
15502 	+/
15503 	mixin template RegisterSetters() {
15504 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
15505 			switch(name) {
15506 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
15507 					case memberName:
15508 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
15509 							if(value != "true" && value != "false")
15510 								return SetPropertyResult.wrongFormat;
15511 							__traits(getMember, this, memberName) = value == "true" ? true : false;
15512 							return SetPropertyResult.success;
15513 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
15514 							import core.stdc.stdlib;
15515 							char[128] zero = 0;
15516 							if(buffer.length + 1 >= zero.length)
15517 								return SetPropertyResult.wrongFormat;
15518 							zero[0 .. buffer.length] = buffer[];
15519 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
15520 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
15521 							import core.stdc.stdlib;
15522 							char[128] zero = 0;
15523 							if(buffer.length + 1 >= zero.length)
15524 								return SetPropertyResult.wrongFormat;
15525 							zero[0 .. buffer.length] = buffer[];
15526 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
15527 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
15528 							__traits(getMember, this, memberName) = value.idup;
15529 						} else {
15530 							return SetPropertyResult.notImplemented;
15531 						}
15532 
15533 				}
15534 				default:
15535 					return super.setPropertyFromString(name, value, valueIsJson);
15536 			}
15537 		}
15538 	}
15539 
15540 	/++
15541 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
15542 
15543 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
15544 
15545 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
15546 		rarely need to use these building blocks directly.
15547 	+/
15548 	mixin template RegisterGetters() {
15549 		override void getPropertiesList(scope void delegate(string name) sink) const {
15550 			super.getPropertiesList(sink);
15551 
15552 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
15553 				sink(memberName);
15554 			}
15555 		}
15556 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
15557 			switch(name) {
15558 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
15559 					case memberName:
15560 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
15561 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
15562 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
15563 							import core.stdc.stdio;
15564 							char[32] buffer;
15565 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
15566 							sink(name, buffer[0 .. len], true);
15567 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
15568 							import core.stdc.stdio;
15569 							char[32] buffer;
15570 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
15571 							sink(name, buffer[0 .. len], true);
15572 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
15573 							sink(name, __traits(getMember, this, memberName), false);
15574 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
15575 						} else {
15576 							sink(name, null, true);
15577 						}
15578 
15579 					return;
15580 				}
15581 				default:
15582 					return super.getPropertyAsString(name, sink);
15583 			}
15584 		}
15585 	}
15586 }
15587 
15588 private struct Stack(T) {
15589 	this(int maxSize) {
15590 		internalLength = 0;
15591 		arr = initialBuffer[];
15592 	}
15593 
15594 	///.
15595 	void push(T t) {
15596 		if(internalLength >= arr.length) {
15597 			auto oldarr = arr;
15598 			if(arr.length < 4096)
15599 				arr = new T[arr.length * 2];
15600 			else
15601 				arr = new T[arr.length + 4096];
15602 			arr[0 .. oldarr.length] = oldarr[];
15603 		}
15604 
15605 		arr[internalLength] = t;
15606 		internalLength++;
15607 	}
15608 
15609 	///.
15610 	T pop() {
15611 		assert(internalLength);
15612 		internalLength--;
15613 		return arr[internalLength];
15614 	}
15615 
15616 	///.
15617 	T peek() {
15618 		assert(internalLength);
15619 		return arr[internalLength - 1];
15620 	}
15621 
15622 	///.
15623 	@property bool empty() {
15624 		return internalLength ? false : true;
15625 	}
15626 
15627 	///.
15628 	private T[] arr;
15629 	private size_t internalLength;
15630 	private T[64] initialBuffer;
15631 	// 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),
15632 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
15633 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
15634 }
15635 
15636 /// 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.
15637 private struct WidgetStream {
15638 
15639 	///.
15640 	@property Widget front() {
15641 		return current.widget;
15642 	}
15643 
15644 	/// Use Widget.tree instead.
15645 	this(Widget start) {
15646 		current.widget = start;
15647 		current.childPosition = -1;
15648 		isEmpty = false;
15649 		stack = typeof(stack)(0);
15650 	}
15651 
15652 	/*
15653 		Handle it
15654 		handle its children
15655 
15656 	*/
15657 
15658 	///.
15659 	void popFront() {
15660 	    more:
15661 	    	if(isEmpty) return;
15662 
15663 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
15664 
15665 		current.childPosition++;
15666 		if(current.childPosition >= current.widget.children.length) {
15667 			if(stack.empty())
15668 				isEmpty = true;
15669 			else {
15670 				current = stack.pop();
15671 				goto more;
15672 			}
15673 		} else {
15674 			stack.push(current);
15675 			current.widget = current.widget.children[current.childPosition];
15676 			current.childPosition = -1;
15677 		}
15678 	}
15679 
15680 	///.
15681 	@property bool empty() {
15682 		return isEmpty;
15683 	}
15684 
15685 	private:
15686 
15687 	struct Current {
15688 		Widget widget;
15689 		int childPosition;
15690 	}
15691 
15692 	Current current;
15693 
15694 	Stack!(Current) stack;
15695 
15696 	bool isEmpty;
15697 }
15698 
15699 
15700 /+
15701 
15702 	I could fix up the hierarchy kinda like this
15703 
15704 	class Widget {
15705 		Widget[] children() { return null; }
15706 	}
15707 	interface WidgetContainer {
15708 		Widget asWidget();
15709 		void addChild(Widget w);
15710 
15711 		// alias asWidget this; // but meh
15712 	}
15713 
15714 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
15715 
15716 	class Layout : Widget, WidgetContainer {}
15717 
15718 	class Window : WidgetContainer {}
15719 
15720 
15721 	All constructors that previously took Widgets should now take WidgetContainers instead
15722 
15723 
15724 
15725 	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".
15726 +/
15727 
15728 /+
15729 	LAYOUTS 2.0
15730 
15731 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
15732 
15733 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
15734 
15735 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
15736 
15737 	and even Paint can just use computedStyle...
15738 
15739 		background color
15740 		font
15741 		border color and style
15742 
15743 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
15744 		please note that many widgets and in some modes will completely ignore properties as they will.
15745 		they are just hints you set, not promises.
15746 
15747 
15748 
15749 
15750 
15751 	So generally the existing virtual functions are just the default for the class. But individual objects
15752 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
15753 +/
15754 
15755 /++
15756 	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.
15757 
15758 	History:
15759 		Added May 24, 2021.
15760 +/
15761 struct WidgetBackground {
15762 	/++
15763 		A background with the given solid color.
15764 	+/
15765 	this(Color color) {
15766 		this.color = color;
15767 	}
15768 
15769 	this(WidgetBackground bg) {
15770 		this = bg;
15771 	}
15772 
15773 	/++
15774 		Creates a widget from the string.
15775 
15776 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
15777 	+/
15778 	static WidgetBackground fromString(string s) {
15779 		return WidgetBackground(Color.fromString(s));
15780 	}
15781 
15782 	/++
15783 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
15784 
15785 		History:
15786 			Made `public` on December 18, 2022 (dub v10.10).
15787 	+/
15788 	Color color;
15789 }
15790 
15791 /++
15792 	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!)
15793 
15794 	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.
15795 
15796 	You should not inherit from this directly, but instead use [VisualTheme].
15797 
15798 	History:
15799 		Added May 8, 2021
15800 +/
15801 abstract class BaseVisualTheme {
15802 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
15803 	abstract void doPaint(Widget widget, WidgetPainter painter);
15804 
15805 	/+
15806 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
15807 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
15808 	+/
15809 
15810 	/++
15811 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
15812 		where the interpretation of the string varies for each property and may include things like measurement units.
15813 	+/
15814 	abstract string getPropertyString(Widget widget, string propertyName);
15815 
15816 	/++
15817 		Default background color of the window. Widgets also use this to simulate transparency.
15818 
15819 		Probably some shade of grey.
15820 	+/
15821 	abstract Color windowBackgroundColor();
15822 	abstract Color widgetBackgroundColor();
15823 	abstract Color foregroundColor();
15824 	abstract Color lightAccentColor();
15825 	abstract Color darkAccentColor();
15826 
15827 	/++
15828 		Colors used to indicate active selections in lists and text boxes, etc.
15829 	+/
15830 	abstract Color selectionForegroundColor();
15831 	/// ditto
15832 	abstract Color selectionBackgroundColor();
15833 
15834 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
15835 
15836 	/++
15837 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
15838 	+/
15839 	abstract OperatingSystemFont defaultFont(int dpi);
15840 
15841 	private OperatingSystemFont[int] defaultFontCache_;
15842 	private OperatingSystemFont defaultFontCached(int dpi) {
15843 		if(dpi !in defaultFontCache_) {
15844 			// FIXME: set this to false if X disconnect or if visual theme changes
15845 			defaultFontCache_[dpi] = defaultFont(dpi);
15846 		}
15847 		return defaultFontCache_[dpi];
15848 	}
15849 }
15850 
15851 /+
15852 	A widget should have:
15853 		classList
15854 		dataset
15855 		attributes
15856 		computedStyles
15857 		state (persistent)
15858 		dynamic state (focused, hover, etc)
15859 +/
15860 
15861 // visualTheme.computedStyle(this).paddingLeft
15862 
15863 
15864 /++
15865 	This is your entry point to create your own visual theme for custom widgets.
15866 
15867 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
15868 
15869 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
15870 +/
15871 abstract class VisualTheme(CRTP) : BaseVisualTheme {
15872 	override string getPropertyString(Widget widget, string propertyName) {
15873 		return null;
15874 	}
15875 
15876 	/+
15877 		mixin StyleOverride!Widget
15878 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
15879 		w.useStyleProperties(dg);
15880 	}
15881 	+/
15882 
15883 	final override void doPaint(Widget widget, WidgetPainter painter) {
15884 		auto derived = cast(CRTP) cast(void*) this;
15885 
15886 		scope void delegate(Widget, WidgetPainter) bestMatch;
15887 		int bestMatchScore;
15888 
15889 		static if(__traits(hasMember, CRTP, "paint"))
15890 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
15891 			static if(is(typeof(overload) Params == __parameters)) {
15892 				static assert(Params.length == 2);
15893 				static assert(is(Params[0] : Widget));
15894 				static assert(is(Params[1] == WidgetPainter));
15895 				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);
15896 
15897 				alias type = Params[0];
15898 				if(cast(type) widget) {
15899 					auto score = baseClassCount!type;
15900 
15901 					if(score > bestMatchScore) {
15902 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
15903 						bestMatchScore = score;
15904 					}
15905 				}
15906 			} else static assert(0, "paint should be a method.");
15907 		}
15908 
15909 		if(bestMatch)
15910 			bestMatch(widget, painter);
15911 		else
15912 			widget.paint(painter);
15913 	}
15914 
15915 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
15916 
15917 	// 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
15918 	// mixin Beautiful95Theme;
15919 	mixin DefaultLightTheme;
15920 
15921 	private static struct Cached {
15922 		// i prolly want to do this
15923 	}
15924 }
15925 
15926 /// ditto
15927 mixin template Beautiful95Theme() {
15928 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
15929 	override Color widgetBackgroundColor() { return Color.white; }
15930 	override Color foregroundColor() { return Color.black; }
15931 	override Color darkAccentColor() { return Color(172, 172, 172); }
15932 	override Color lightAccentColor() { return Color(223, 223, 223); }
15933 	override Color selectionForegroundColor() { return Color.white; }
15934 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
15935 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
15936 }
15937 
15938 /// ditto
15939 mixin template DefaultLightTheme() {
15940 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
15941 	override Color widgetBackgroundColor() { return Color.white; }
15942 	override Color foregroundColor() { return Color.black; }
15943 	override Color darkAccentColor() { return Color(172, 172, 172); }
15944 	override Color lightAccentColor() { return Color(223, 223, 223); }
15945 	override Color selectionForegroundColor() { return Color.white; }
15946 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
15947 	override OperatingSystemFont defaultFont(int dpi) {
15948 		version(Windows)
15949 			return new OperatingSystemFont("Segoe UI");
15950 		else {
15951 			// FIXME: undo xft's scaling so we don't end up double scaled
15952 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
15953 		}
15954 	}
15955 }
15956 
15957 /// ditto
15958 mixin template DefaultDarkTheme() {
15959 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
15960 	override Color widgetBackgroundColor() { return Color.black; }
15961 	override Color foregroundColor() { return Color.white; }
15962 	override Color darkAccentColor() { return Color(20, 20, 20); }
15963 	override Color lightAccentColor() { return Color(80, 80, 80); }
15964 	override Color selectionForegroundColor() { return Color.white; }
15965 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
15966 	override OperatingSystemFont defaultFont(int dpi) {
15967 		version(Windows)
15968 			return new OperatingSystemFont("Segoe UI", 12);
15969 		else
15970 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
15971 	}
15972 }
15973 
15974 /// ditto
15975 alias DefaultTheme = DefaultLightTheme;
15976 
15977 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
15978 	/+
15979 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
15980 	Color windowBackgroundColor() { return Color(242, 242, 242); }
15981 	Color darkAccentColor() { return windowBackgroundColor; }
15982 	Color lightAccentColor() { return windowBackgroundColor; }
15983 	+/
15984 }
15985 
15986 /++
15987 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
15988 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
15989 
15990 	History:
15991 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
15992 +/
15993 class StateChanged(alias field) : Event {
15994 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
15995 	override bool cancelable() const { return false; }
15996 	this(Widget target, typeof(field) newValue) {
15997 		this.newValue = newValue;
15998 		super(EventString, target);
15999 	}
16000 
16001 	typeof(field) newValue;
16002 }
16003 
16004 /++
16005 	Convenience function to add a `triggered` event listener.
16006 
16007 	Its implementation is simply `w.addEventListener("triggered", dg);`
16008 
16009 	History:
16010 		Added November 27, 2021 (dub v10.4)
16011 +/
16012 void addWhenTriggered(Widget w, void delegate() dg) {
16013 	w.addEventListener("triggered", dg);
16014 }
16015 
16016 /++
16017 	Observable varables can be added to widgets and when they are changed, it fires
16018 	off a [StateChanged] event so you can react to it.
16019 
16020 	It is implemented as a getter and setter property, along with another helper you
16021 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
16022 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
16023 	example.
16024 
16025 	History:
16026 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16027 +/
16028 mixin template Observable(T, string name) {
16029 	private T backing;
16030 
16031 	mixin(q{
16032 		void } ~ name ~ q{_changed (void delegate(T) dg) {
16033 			this.addEventListener((StateChanged!this_thing ev) {
16034 				dg(ev.newValue);
16035 			});
16036 		}
16037 
16038 		@property T } ~ name ~ q{ () {
16039 			return backing;
16040 		}
16041 
16042 		@property void } ~ name ~ q{ (T t) {
16043 			backing = t;
16044 			auto event = new StateChanged!this_thing(this, t);
16045 			event.dispatch();
16046 		}
16047 	});
16048 
16049 	mixin("private alias this_thing = " ~ name ~ ";");
16050 }
16051 
16052 
16053 private bool startsWith(string test, string thing) {
16054 	if(test.length < thing.length)
16055 		return false;
16056 	return test[0 .. thing.length] == thing;
16057 }
16058 
16059 private bool endsWith(string test, string thing) {
16060 	if(test.length < thing.length)
16061 		return false;
16062 	return test[$ - thing.length .. $] == thing;
16063 }
16064 
16065 // still do layout delegation
16066 // and... split off Window from Widget.
16067 
16068 version(minigui_screenshots)
16069 struct Screenshot {
16070 	string name;
16071 }
16072 
16073 version(minigui_screenshots)
16074 static if(__VERSION__ > 2092)
16075 mixin(q{
16076 shared static this() {
16077 	import core.runtime;
16078 
16079 	static UnitTestResult screenshotMagic() {
16080 		string name;
16081 
16082 		import arsd.png;
16083 
16084 		auto results = new Window();
16085 		auto button = new Button("do it", results);
16086 
16087 		Window.newWindowCreated = delegate(Window w) {
16088 			Timer timer;
16089 			timer = new Timer(250, {
16090 				auto img = w.win.takeScreenshot();
16091 				timer.destroy();
16092 
16093 				version(Windows)
16094 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
16095 				else
16096 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
16097 
16098 				w.close();
16099 			});
16100 		};
16101 
16102 		button.addWhenTriggered( {
16103 
16104 		foreach(test; __traits(getUnitTests, mixin(__MODULE__))) {
16105 			name = null;
16106 			static foreach(attr; __traits(getAttributes, test)) {
16107 				static if(is(typeof(attr) == Screenshot))
16108 					name = attr.name;
16109 			}
16110 			if(name.length) {
16111 				test();
16112 			}
16113 		}
16114 
16115 		});
16116 
16117 		results.loop();
16118 
16119 		return UnitTestResult(0, 0, false, false);
16120 	}
16121 
16122 
16123 	Runtime.extendedModuleUnitTester = &screenshotMagic;
16124 }
16125 });
16126 version(minigui_screenshots) {
16127 	version(unittest)
16128 		void main() {}
16129 	else static assert(0, "dont forget the -unittest flag to dmd");
16130 }
16131 
16132 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
16133 // FIXME: make multiple accelerators disambiguate based ona rgs
16134 // FIXME: MainWindow ctor should have same arg order as Window
16135 // FIXME: mainwindow ctor w/ client area size instead of total size.
16136 // 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.
16137 // FIXME: tri-state checkbox
16138 // FIXME: subordinate controls grouping...