1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
2 
3 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay
4 
5 // me@arsd:~/.kde/share/config$ vim kdeglobals
6 
7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too.
8 
9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/
10 
11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever.
12 
13 // responsive minigui, menu search, and file open with a preview hook on the side.
14 
15 // FIXME: add menu checkbox and menu icon eventually
16 
17 /*
18 
19 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan
20 
21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it...
22 */
23 
24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places
25 
26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk.
27 
28 // FIXME: opt-in file picker widget with image support
29 
30 // FIXME: number widget
31 
32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm
33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles
34 
35 // osx style menu search.
36 
37 // would be cool for a scroll bar to have marking capabilities
38 // kinda like vim's marks just on clicks etc and visual representation
39 // generically. may be cool to add an up arrow to the bottom too
40 //
41 // leave a shadow of where you last were for going back easily
42 
43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various
44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for
45 // the window.
46 
47 // so what about context menus?
48 
49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
50 
51 // FIXME: make the scroll thing go to bottom when the content changes.
52 
53 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image
54 
55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
56 
57 
58 // FIXME: add a command search thingy built in and implement tip.
59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!
60 
61 // On Windows:
62 // FIXME: various labels look broken in high contrast mode
63 // FIXME: changing themes while the program is upen doesn't trigger a redraw
64 
65 // add note about manifest to documentation. also icons.
66 
67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar
68 // FIXME: clear the corner of scrollbars if they pop up
69 
70 // minigui needs to have a stdout redirection for gui mode on windows writeln
71 
72 // I kinda wanna do state reacting. sort of. idk tho
73 
74 // need a viewer widget that works like a web page - arrows scroll down consistently
75 
76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside.
77 
78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two.
79 // and help info about menu items.
80 // and search in menus?
81 
82 // FIXME: a scroll area event signaling when a thing comes into view might be good
83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must
84 
85 // FIXME: unify Windows style line endings
86 
87 /*
88 	TODO:
89 
90 	pie menu
91 
92 	class Form with submit behavior -- see AutomaticDialog
93 
94 	disabled widgets and menu items
95 
96 	event cleanup
97 	tooltips.
98 	api improvements
99 
100 	margins are kinda broken, they don't collapse like they should. at least.
101 
102 	a table form btw would be a horizontal layout of vertical layouts holding each column
103 	that would give the same width things
104 */
105 
106 /*
107 
108 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets
109 */
110 
111 /++
112 	minigui is a smallish GUI widget library, aiming to be on par with at least
113 	HTML4 forms and a few other expected gui components. It uses native controls
114 	on Windows and does its own thing on Linux (Mac is not currently supported but
115 	may be later, and should use native controls) to keep size down. The Linux
116 	appearance is similar to Windows 95 and avoids using images to maintain network
117 	efficiency on remote X connections, though you can customize that.
118 
119 
120 	minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color],
121 	on which it is built. simpledisplay provides the low-level interfaces and minigui
122 	builds the concept of widgets inside the windows on top of it.
123 
124 	Its #1 goal is to be useful without being large and complicated like GTK and Qt.
125 	It isn't hugely concerned with appearance - on Windows, it just uses the native
126 	controls and native theme, and on Linux, it keeps it simple and I may change that
127 	at any time, though after May 2021, you can customize some things with css-inspired
128 	[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
129 	you can use the custom implementation there too, but... you shouldn't.)
130 
131 	The event model is similar to what you use in the browser with Javascript and the
132 	layout engine tries to automatically fit things in, similar to a css flexbox.
133 
134 	FOR BEST RESULTS: be sure to link with the appropriate subsystem command
135 	`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
136 	of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
137 
138 	Otherwise you'll get a console and possibly other visual bugs. But if you do use
139 	the subsystem:windows, note that Phobos' writeln will crash the program!
140 
141 	HTML_To_Classes:
142 	$(SMALL_TABLE
143 		HTML Code | Minigui Class
144 
145 		`<input type="text">` | [LineEdit]
146 		`<textarea>` | [TextEdit]
147 		`<select>` | [DropDownSelection]
148 		`<input type="checkbox">` | [Checkbox]
149 		`<input type="radio">` | [Radiobox]
150 		`<button>` | [Button]
151 	)
152 
153 
154 	Stretchiness:
155 		The default is 4. You can use larger numbers for things that should
156 		consume a lot of space, and lower numbers for ones that are better at
157 		smaller sizes.
158 
159 	Overlapped_input:
160 		COMING EVENTUALLY:
161 		minigui will include a little bit of I/O functionality that just works
162 		with the event loop. If you want to get fancy, I suggest spinning up
163 		another thread and posting events back and forth.
164 
165 	$(H2 Add ons)
166 		See the `minigui_addons` directory in the arsd repo for some add on widgets
167 		you can import separately too.
168 
169 	$(H3 XML definitions)
170 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
171 
172 	$(H3 Scriptability)
173 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
174 		in this documentation, it means you can call it from the script language.
175 
176 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
177 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
178 
179 		---
180 		import arsd.minigui_xml;
181 		import arsd.script;
182 
183 		var globals = var.emptyObject;
184 		globals.makeWidgetFromString = &makeWidgetFromString;
185 
186 		// this now works
187 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
188 		---
189 
190 		More to come.
191 
192 	History:
193 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
194 
195 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
196 		tag this as version 2.0.
197 
198 		Among the changes:
199 		$(LIST
200 			* The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard.
201 
202 			See [Event] for details.
203 
204 			* A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead.
205 
206 			See [DoubleClickEvent] for details.
207 
208 			* Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows.
209 
210 			See [Widget.Style] for details.
211 
212 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
213 
214 			* Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it.
215 
216 			* Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst.
217 
218 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
219 
220 			* Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does.
221 
222 			* Various non-breaking additions.
223 		)
224 +/
225 module arsd.minigui;
226 
227 /++
228 	This hello world sample will have an oversized button, but that's ok, you see your first window!
229 +/
230 version(Demo)
231 unittest {
232 	import arsd.minigui;
233 
234 	void main() {
235 		auto window = new MainWindow();
236 
237 		// note the parent widget is almost always passed as the last argument to a constructor
238 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
239 		auto button = new Button("Close", window);
240 		button.addWhenTriggered({
241 			window.close();
242 		});
243 
244 		window.loop();
245 	}
246 
247 	main(); // exclude from docs
248 }
249 
250 /++
251 	This example shows one way you can partition your window into a header
252 	and sidebar. Here, the header and sidebar have a fixed width, while the
253 	rest of the content sizes with the window.
254 
255 	It might be a new way of thinking about window layout to do things this
256 	way - perhaps [GridLayout] more matches your style of thought - but the
257 	concept here is to partition the window into sub-boxes with a particular
258 	size, then partition those boxes into further boxes.
259 
260 	$(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.)
261 
262 	So to make the header, start with a child layout that has a max height.
263 	It will use that space from the top, then the remaining children will
264 	split the remaining area, meaning you can think of is as just being another
265 	box you can split again. Keep splitting until you have the look you desire.
266 +/
267 // https://github.com/adamdruppe/arsd/issues/310
268 version(minigui_screenshots)
269 @Screenshot("layout")
270 unittest {
271 	import arsd.minigui;
272 
273 	// This helper class is just to help make the layout boxes visible.
274 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
275 	class ColorWidget : Widget {
276 		this(Color color, Widget parent) {
277 			this.color = color;
278 			super(parent);
279 		}
280 		Color color;
281 		class Style : Widget.Style {
282 			override WidgetBackground background() { return WidgetBackground(color); }
283 		}
284 		mixin OverrideStyle!Style;
285 	}
286 
287 	void main() {
288 		auto window = new Window;
289 
290 		// the key is to give it a max height. This is one way to do it:
291 		auto header = new class HorizontalLayout {
292 			this() { super(window); }
293 			override int maxHeight() { return 50; }
294 		};
295 		// this next line is a shortcut way of doing it too, but it only works
296 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
297 		// is good to know how to make a new class like above anyway.
298 		// auto header = new HorizontalLayout(50, window);
299 
300 		auto bar = new HorizontalLayout(window);
301 
302 		// or since this is so common, VerticalLayout and HorizontalLayout both
303 		// can just take an argument in their constructor for max width/height respectively
304 
305 		// (could have tone this above too, but I wanted to demo both techniques)
306 		auto left = new VerticalLayout(100, bar);
307 
308 		// and this is the main section's container. A plain Widget instance is good enough here.
309 		auto container = new Widget(bar);
310 
311 		// and these just add color to the containers we made above for the screenshot.
312 		// in a real application, you can just add your actual controls instead of these.
313 		auto headerColorBox = new ColorWidget(Color.teal, header);
314 		auto leftColorBox = new ColorWidget(Color.green, left);
315 		auto rightColorBox = new ColorWidget(Color.purple, container);
316 
317 		window.loop();
318 	}
319 
320 	main(); // exclude from docs
321 }
322 
323 
324 import arsd.core;
325 alias Timer = arsd.simpledisplay.Timer;
326 public import arsd.simpledisplay;
327 /++
328 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
329 
330 	History:
331 		Was private until May 15, 2021.
332 +/
333 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
334 
335 version(Windows) {
336 	import core.sys.windows.winnls;
337 	import core.sys.windows.windef;
338 	import core.sys.windows.basetyps;
339 	import core.sys.windows.winbase;
340 	import core.sys.windows.winuser;
341 	import core.sys.windows.wingdi;
342 	static import gdi = core.sys.windows.wingdi;
343 }
344 
345 version(Windows) {
346 	version(minigui_manifest) {} else version=minigui_no_manifest;
347 
348 	version(minigui_no_manifest) {} else
349 	static if(__VERSION__ >= 2_083)
350 	version(CRuntime_Microsoft) { // FIXME: mingw?
351 		// assume we want commctrl6 whenever possible since there's really no reason not to
352 		// and this avoids some of the manifest hassle
353 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
354 	}
355 }
356 
357 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
358 private bool lastDefaultPrevented;
359 
360 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
361 alias scriptable = arsd_jsvar_compatible;
362 
363 version(Windows) {
364 	// use native widgets when available unless specifically asked otherwise
365 	version(custom_widgets) {
366 		enum bool UsingCustomWidgets = true;
367 		enum bool UsingWin32Widgets = false;
368 	} else {
369 		version = win32_widgets;
370 		enum bool UsingCustomWidgets = false;
371 		enum bool UsingWin32Widgets = true;
372 
373 		// give access to my text system for the rich text cross platform stuff
374 		version = use_new_text_system;
375 		import arsd.textlayouter;
376 	}
377 	// and native theming when needed
378 	//version = win32_theming;
379 } else {
380 	enum bool UsingCustomWidgets = true;
381 	enum bool UsingWin32Widgets = false;
382 	version=custom_widgets;
383 }
384 
385 
386 
387 /*
388 
389 	The main goals of minigui.d are to:
390 		1) Provide basic widgets that just work in a lightweight lib.
391 		   I basically want things comparable to a plain HTML form,
392 		   plus the easy and obvious things you expect from Windows
393 		   apps like a menu.
394 		2) Use native things when possible for best functionality with
395 		   least library weight.
396 		3) Give building blocks to provide easy extension for your
397 		   custom widgets, or hooking into additional native widgets
398 		   I didn't wrap.
399 		4) Provide interfaces for easy interaction between third
400 		   party minigui extensions. (event model, perhaps
401 		   signals/slots, drop-in ease of use bits.)
402 		5) Zero non-system dependencies, including Phobos as much as
403 		   I reasonably can. It must only import arsd.color and
404 		   my simpledisplay.d. If you need more, it will have to be
405 		   an extension module.
406 		6) An easy layout system that generally works.
407 
408 	A stretch goal is to make it easy to make gui forms with code,
409 	some kind of resource file (xml?) and even a wysiwyg designer.
410 
411 	Another stretch goal is to make it easy to hook data into the gui,
412 	including from reflection. So like auto-generate a form from a
413 	function signature or struct definition, or show a list from an
414 	array that automatically updates as the array is changed. Then,
415 	your program focuses on the data more than the gui interaction.
416 
417 
418 
419 	STILL NEEDED:
420 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
421 		* slider
422 		* listbox
423 		* spinner
424 		* label?
425 		* rich text
426 */
427 
428 
429 /+
430 	enum LayoutMethods {
431 		 verticalFlex,
432 		 horizontalFlex,
433 		 inlineBlock, // left to right, no stretch, goes to next line as needed
434 		 static, // just set to x, y
435 		 verticalNoStretch, // browser style default
436 
437 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
438 
439 		 grid, // magic
440 	}
441 +/
442 
443 /++
444 	The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own.
445 
446 
447 	To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional.
448 
449 	---
450 	class MinimalWidget : Widget {
451 		this(Widget parent) {
452 			super(parent);
453 		}
454 	}
455 	---
456 
457 	$(SIDEBAR
458 		I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway.
459 	)
460 
461 	Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal.
462 
463 	Among the things you'll most likely want to change in your custom widget:
464 
465 	$(LIST
466 		* In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.)
467 
468 		You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now.
469 
470 		Do this $(I after) calling the `super` constructor.
471 
472 		* Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget.
473 
474 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
475 
476 		* Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model.
477 
478 		* You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want.
479 	)
480 
481 	On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s.
482 
483 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
484 
485 	Your own custom-drawn and native system controls can exist side-by-side.
486 
487 	Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas.
488 +/
489 class Widget : ReflectableProperties {
490 
491 	private bool willDraw() {
492 		return true;
493 	}
494 
495 	/+
496 	/++
497 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
498 
499 		History:
500 			Added September 15, 2021
501 			implemented.... ???
502 	+/
503 	void prepareReflection(this This)() {
504 
505 	}
506 	+/
507 
508 	private bool _enabled = true;
509 
510 	/++
511 		Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled.
512 
513 		I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it.
514 
515 		History:
516 			Added November 23, 2021 (dub v10.4)
517 
518 			Warning: the specific behavior of disabling with parents may change in the future.
519 		Bugs:
520 			Currently only implemented for widgets backed by native Windows controls.
521 
522 		See_Also: [disabledReason], [disabledBy]
523 	+/
524 	@property bool enabled() {
525 		return disabledBy() is null;
526 	}
527 
528 	/// ditto
529 	@property void enabled(bool yes) {
530 		_enabled = yes;
531 		version(win32_widgets) {
532 			if(hwnd)
533 				EnableWindow(hwnd, yes);
534 		}
535 		setDynamicState(DynamicState.disabled, yes);
536 	}
537 
538 	private string disabledReason_;
539 
540 	/++
541 		If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control.
542 
543 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
544 
545 		History:
546 			Added November 23, 2021 (dub v10.4)
547 		See_Also: [enabled], [disabledBy]
548 	+/
549 	@property string disabledReason() {
550 		auto w = disabledBy();
551 		return (w is null) ? null : w.disabledReason_;
552 	}
553 
554 	/// ditto
555 	@property void disabledReason(string reason) {
556 		disabledReason_ = reason;
557 	}
558 
559 	/++
560 		Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user.
561 
562 		History:
563 			Added November 25, 2021 (dub v10.4)
564 		See_Also: [enabled], [disabledReason]
565 	+/
566 	Widget disabledBy() {
567 		Widget p = this;
568 		while(p) {
569 			if(!p._enabled)
570 				return p;
571 			p = p.parent;
572 		}
573 		return null;
574 	}
575 
576 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
577 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
578 		if(valueIsJson)
579 			return SetPropertyResult.wrongFormat;
580 		switch(name) {
581 			case "name":
582 				this.name = value.idup;
583 				return SetPropertyResult.success;
584 			case "statusTip":
585 				this.statusTip = value.idup;
586 				return SetPropertyResult.success;
587 			default:
588 				return SetPropertyResult.noSuchProperty;
589 		}
590 	}
591 	/// ditto
592 	void getPropertiesList(scope void delegate(string name) sink) const {
593 		sink("name");
594 		sink("statusTip");
595 	}
596 	/// ditto
597 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
598 		switch(name) {
599 			case "name":
600 				sink(name, this.name, false);
601 				return;
602 			case "statusTip":
603 				sink(name, this.statusTip, false);
604 				return;
605 			default:
606 				sink(name, null, true);
607 		}
608 	}
609 
610 	/++
611 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
612 
613 		History:
614 			Added November 25, 2021 (dub v10.5)
615 			`Point` overload added January 12, 2022 (dub v10.6)
616 	+/
617 	int scaleWithDpi(int value, int assumedDpi = 96) {
618 		// avoid potential overflow with common special values
619 		if(value == int.max)
620 			return int.max;
621 		if(value == int.min)
622 			return int.min;
623 		if(value == 0)
624 			return 0;
625 		return value * currentDpi(assumedDpi) / assumedDpi;
626 	}
627 
628 	/// ditto
629 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
630 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
631 	}
632 
633 	/++
634 		Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor.
635 
636 		Not entirely stable.
637 
638 		History:
639 			Added August 25, 2023 (dub v11.1)
640 	+/
641 	final int currentDpi(int assumedDpi = 96) {
642 		// assert(parentWindow !is null);
643 		// assert(parentWindow.win !is null);
644 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
645 		//divide = 138; // to test 1.5x
646 		// for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down.
647 		// this also covers the case when actualDpi returns 0.
648 		if(divide < 96)
649 			divide = 96;
650 		return divide;
651 	}
652 
653 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
654 	// I'll think up something better eventually
655 
656 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
657 	protected final int defaultLineHeight() {
658 		auto cs = getComputedStyle();
659 		if(cs.font && !cs.font.isNull)
660 			return cs.font.height() * 5 / 4;
661 		else
662 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
663 	}
664 
665 	/++
666 
667 		History:
668 			Added August 25, 2023 (dub v11.1)
669 	+/
670 	protected final int defaultTextHeight(int numberOfLines = 1) {
671 		auto cs = getComputedStyle();
672 		if(cs.font && !cs.font.isNull)
673 			return cs.font.height() * numberOfLines;
674 		else
675 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
676 	}
677 
678 	protected final int defaultTextWidth(const(char)[] text) {
679 		auto cs = getComputedStyle();
680 		if(cs.font && !cs.font.isNull)
681 			return cs.font.stringWidth(text);
682 		else
683 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
684 	}
685 
686 	/++
687 		If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget.
688 
689 		The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you.
690 
691 		History:
692 			Added May 22, 2021
693 	+/
694 	protected bool encapsulatedChildren() {
695 		return false;
696 	}
697 
698 	private void privateDpiChanged() {
699 		dpiChanged();
700 		foreach(child; children)
701 			child.privateDpiChanged();
702 	}
703 
704 	/++
705 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
706 
707 		History:
708 			Added January 12, 2022 (dub v10.6)
709 	+/
710 	protected void dpiChanged() {
711 
712 	}
713 
714 	// Default layout properties {
715 
716 		int minWidth() { return 0; }
717 		int minHeight() {
718 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
719 			int sum = this.paddingTop + this.paddingBottom;
720 			foreach(child; children) {
721 				if(child.hidden)
722 					continue;
723 				sum += child.minHeight();
724 				sum += child.marginTop();
725 				sum += child.marginBottom();
726 			}
727 
728 			return sum;
729 		}
730 		int maxWidth() { return int.max; }
731 		int maxHeight() { return int.max; }
732 		int widthStretchiness() { return 4; }
733 		int heightStretchiness() { return 4; }
734 
735 		/++
736 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
737 
738 			History:
739 				Added June 15, 2021 (dub v10.1)
740 		+/
741 		int widthShrinkiness() { return 0; }
742 		/// ditto
743 		int heightShrinkiness() { return 0; }
744 
745 		/++
746 			The initial size of the widget for layout calculations. Default is 0.
747 
748 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
749 
750 			History:
751 				Added June 15, 2021 (dub v10.1)
752 		+/
753 		int flexBasisWidth() { return 0; }
754 		/// ditto
755 		int flexBasisHeight() { return 0; }
756 
757 		/++
758 			Not stable.
759 
760 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
761 
762 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
763 
764 			History:
765 				Added January 5, 2023
766 		+/
767 		Rectangle defaultMargin;
768 		/// ditto
769 		Rectangle defaultPadding;
770 
771 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
772 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
773 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
774 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
775 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
776 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
777 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
778 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
779 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
780 
781 		private bool recomputeChildLayoutRequired = true;
782 		private static class RecomputeEvent {}
783 		private __gshared rce = new RecomputeEvent();
784 		protected final void queueRecomputeChildLayout() {
785 			recomputeChildLayoutRequired = true;
786 
787 			if(this.parentWindow) {
788 				auto sw = this.parentWindow.win;
789 				assert(sw !is null);
790 				if(!sw.eventQueued!RecomputeEvent) {
791 					sw.postEvent(rce);
792 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
793 				}
794 			}
795 
796 		}
797 
798 		protected final void recomputeChildLayoutEntry() {
799 			if(recomputeChildLayoutRequired) {
800 				recomputeChildLayout();
801 				recomputeChildLayoutRequired = false;
802 				redraw();
803 			} else {
804 				// I still need to check the tree just in case one of them was queued up
805 				// and the event came up here instead of there.
806 				foreach(child; children)
807 					child.recomputeChildLayoutEntry();
808 			}
809 		}
810 
811 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
812 		void recomputeChildLayout() {
813 			.recomputeChildLayout!"height"(this);
814 		}
815 
816 	// }
817 
818 
819 	/++
820 		Returns the style's tag name string this object uses.
821 
822 		The default is to use the typeid() name trimmed down to whatever is after the last dot which is typically the identifier of the class.
823 
824 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
825 
826 		History:
827 			Added May 10, 2021
828 	+/
829 	string styleTagName() const {
830 		string n = typeid(this).name;
831 		foreach_reverse(idx, ch; n)
832 			if(ch == '.') {
833 				n = n[idx + 1 .. $];
834 				break;
835 			}
836 		return n;
837 	}
838 
839 	/// API for the [styleClassList]
840 	static struct ClassList {
841 		private Widget widget;
842 
843 		///
844 		void add(string s) {
845 			widget.styleClassList_ ~= s;
846 		}
847 
848 		///
849 		void remove(string s) {
850 			foreach(idx, s1; widget.styleClassList_)
851 				if(s1 == s) {
852 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
853 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
854 					widget.styleClassList_.assumeSafeAppend();
855 					return;
856 				}
857 		}
858 
859 		/// Returns true if it was added, false if it was removed.
860 		bool toggle(string s) {
861 			if(contains(s)) {
862 				remove(s);
863 				return false;
864 			} else {
865 				add(s);
866 				return true;
867 			}
868 		}
869 
870 		///
871 		bool contains(string s) const {
872 			foreach(s1; widget.styleClassList_)
873 				if(s1 == s)
874 					return true;
875 			return false;
876 
877 		}
878 	}
879 
880 	private string[] styleClassList_;
881 
882 	/++
883 		Returns a "class list" that can be used by the visual theme's style engine via [VisualTheme.getPropertyString] if it chooses to do something like CSS.
884 
885 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
886 
887 		History:
888 			Added May 10, 2021
889 	+/
890 	inout(ClassList) styleClassList() inout {
891 		return cast(inout(ClassList)) ClassList(cast() this);
892 	}
893 
894 	/++
895 		List of dynamic states made available to the style engine, for cases like CSS pseudo-classes and also used by default paint methods. It is stored in a 64 bit variable attached to the widget that you can update. The style cache is aware of the fact that these can frequently change.
896 
897 		The lower 32 bits are defined here or reserved for future use by the library. You should keep these updated if you reasonably can on custom widgets if they apply to you, but don't use them for a purpose they aren't defined for.
898 
899 		The upper 32 bits are available for your own extensions.
900 
901 		History:
902 			Added May 10, 2021
903 	+/
904 	enum DynamicState : ulong {
905 		focus = (1 << 0), /// the widget currently has the keyboard focus
906 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
907 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!)
908 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!)
909 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
910 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
911 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
912 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
913 		depressed = (1 << 8), /// the widget is being actively pressed or clicked (compare to css `:active`). Can be combined with hover to visually indicate if a mouse up would result in a click event.
914 
915 		USER_BEGIN = (1UL << 32),
916 	}
917 
918 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
919 
920 	/// ditto
921 	@property ulong dynamicState() { return dynamicState_; }
922 	/// ditto
923 	@property ulong dynamicState(ulong newValue) {
924 		if(dynamicState != newValue) {
925 			auto old = dynamicState_;
926 			dynamicState_ = newValue;
927 
928 			useStyleProperties((scope Widget.Style s) {
929 				if(s.variesWithState(old ^ newValue))
930 					redraw();
931 			});
932 		}
933 		return dynamicState_;
934 	}
935 
936 	/// ditto
937 	void setDynamicState(ulong flags, bool state) {
938 		auto ds = dynamicState_;
939 		if(state)
940 			ds |= flags;
941 		else
942 			ds &= ~flags;
943 
944 		dynamicState = ds;
945 	}
946 
947 	private ulong dynamicState_;
948 
949 	deprecated("Use dynamic styles instead now") {
950 		Color backgroundColor() { return backgroundColor_; }
951 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
952 
953 		MouseCursor cursor() { return GenericCursor.Default; }
954 	} private Color backgroundColor_ = Color.transparent;
955 
956 
957 	/++
958 		Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such).
959 
960 		It is here so there can be a specificity switch.
961 
962 		See [OverrideStyle] for a helper function to use your own.
963 
964 		History:
965 			Added May 11, 2021
966 	+/
967 	static class Style/* : StyleProperties*/ {
968 		public Widget widget; // public because the mixin template needs access to it
969 
970 		/++
971 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
972 
973 			History:
974 				Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect.
975 		+/
976 		bool variesWithState(ulong dynamicStateFlags) {
977 			version(win32_widgets) {
978 				if(widget.hwnd)
979 					return false;
980 			}
981 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
982 		}
983 
984 		///
985 		Color foregroundColor() {
986 			return WidgetPainter.visualTheme.foregroundColor;
987 		}
988 
989 		///
990 		WidgetBackground background() {
991 			// the default is a "transparent" background, which means
992 			// it goes as far up as it can to get the color
993 			if (widget.backgroundColor_ != Color.transparent)
994 				return WidgetBackground(widget.backgroundColor_);
995 			if (widget.parent)
996 				return widget.parent.getComputedStyle.background;
997 			return WidgetBackground(widget.backgroundColor_);
998 		}
999 
1000 		private static OperatingSystemFont fontCached_;
1001 		private OperatingSystemFont fontCached() {
1002 			if(fontCached_ is null)
1003 				fontCached_ = font();
1004 			return fontCached_;
1005 		}
1006 
1007 		/++
1008 			Returns the default font to be used with this widget. The return value will be cached by the library, so you can not expect live updates.
1009 		+/
1010 		OperatingSystemFont font() {
1011 			return null;
1012 		}
1013 
1014 		/++
1015 			Returns the cursor that should be used over this widget. You may change this and updates will be reflected next time the mouse enters the widget.
1016 
1017 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1018 
1019 			History:
1020 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1021 		+/
1022 		MouseCursor cursor() {
1023 			return GenericCursor.Default;
1024 		}
1025 
1026 		FrameStyle borderStyle() {
1027 			return FrameStyle.none;
1028 		}
1029 
1030 		/++
1031 		+/
1032 		Color borderColor() {
1033 			return Color.transparent;
1034 		}
1035 
1036 		FrameStyle outlineStyle() {
1037 			if(widget.dynamicState & DynamicState.focus)
1038 				return FrameStyle.dotted;
1039 			else
1040 				return FrameStyle.none;
1041 		}
1042 
1043 		Color outlineColor() {
1044 			return foregroundColor;
1045 		}
1046 	}
1047 
1048 	/++
1049 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1050 		The basic usage is simple:
1051 
1052 		---
1053 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1054 			// override style hints as-needed here
1055 		}
1056 		OverrideStyle!Style; // add the method
1057 		---
1058 
1059 		$(TIP
1060 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1061 			can not be inherited by other objects whereas the static one can. A property on the base class,
1062 			called [Widget.Style.widget|widget], is available for you to access its properties.
1063 		)
1064 
1065 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1066 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1067 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1068 
1069 
1070 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1071 		You may also just override `variesWithState` when you use this flag.
1072 
1073 		---
1074 		mixin OverrideStyle!(
1075 			DynamicState.focus, YourFocusedStyle,
1076 			DynamicState.hover, YourHoverStyle,
1077 			YourDefaultStyle
1078 		)
1079 		---
1080 
1081 		It checks if `dynamicState` matches the state and if so, returns the object given.
1082 
1083 		If there is no state mask given, the next one matches everything. The first match given is used.
1084 
1085 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1086 		find much use for this whole-class swap out.
1087 
1088 		History:
1089 			Added May 16, 2021
1090 	+/
1091 	static protected mixin template OverrideStyle(S...) {
1092 		static import amg = arsd.minigui;
1093 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1094 			ulong mask = 0;
1095 			foreach(idx, thing; S) {
1096 				static if(is(typeof(thing) : ulong)) {
1097 					mask = thing;
1098 				} else {
1099 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1100 						//static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class.");
1101 						scope amg.Widget.Style s = new thing();
1102 						s.widget = this;
1103 						dg(s);
1104 						return;
1105 					}
1106 				}
1107 			}
1108 		}
1109 	}
1110 	/++
1111 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1112 	+/
1113 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1114 		scope Style s = new Style();
1115 		s.widget = this;
1116 		dg(s);
1117 	}
1118 
1119 
1120 	protected void sendResizeEvent() {
1121 		this.emit!ResizeEvent();
1122 	}
1123 
1124 	Menu contextMenu(int x, int y) { return null; }
1125 
1126 	final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) {
1127 		if(parentWindow is null || parentWindow.win is null) return false;
1128 
1129 		auto menu = this.contextMenu(x, y);
1130 		if(menu is null)
1131 			return false;
1132 
1133 		version(win32_widgets) {
1134 			// FIXME: if it is -1, -1, do it at the current selection location instead
1135 			// tho the corner of the window, whcih it does now, isn't the literal worst.
1136 
1137 			if(screenX < 0 && screenY < 0) {
1138 				auto p = this.globalCoordinates();
1139 				if(screenX == -2)
1140 					p.x += x;
1141 				if(screenY == -2)
1142 					p.y += y;
1143 
1144 				screenX = p.x;
1145 				screenY = p.y;
1146 			}
1147 
1148 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1149 				throw new Exception("TrackContextMenuEx");
1150 		} else version(custom_widgets) {
1151 			menu.popup(this, x, y);
1152 		}
1153 
1154 		return true;
1155 	}
1156 
1157 	/++
1158 		Removes this widget from its parent.
1159 
1160 		History:
1161 			`removeWidget` was made `final` on May 11, 2021.
1162 	+/
1163 	@scriptable
1164 	final void removeWidget() {
1165 		auto p = this.parent;
1166 		if(p) {
1167 			int item;
1168 			for(item = 0; item < p._children.length; item++)
1169 				if(p._children[item] is this)
1170 					break;
1171 			auto idx = item;
1172 			for(; item < p._children.length - 1; item++)
1173 				p._children[item] = p._children[item + 1];
1174 			p._children = p._children[0 .. $-1];
1175 
1176 			this.parent.widgetRemoved(idx, this);
1177 			//this.parent = null;
1178 
1179 			p.queueRecomputeChildLayout();
1180 		}
1181 		version(win32_widgets) {
1182 			removeAllChildren();
1183 			if(hwnd) {
1184 				DestroyWindow(hwnd);
1185 				hwnd = null;
1186 			}
1187 		}
1188 	}
1189 
1190 	/++
1191 		Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync.
1192 
1193 		History:
1194 			Added September 19, 2021
1195 	+/
1196 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1197 
1198 	/++
1199 		Removes all child widgets from `this`. You should not use the removed widgets again.
1200 
1201 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1202 
1203 		History:
1204 			Added July 1, 2021 (dub v10.2)
1205 	+/
1206 	void removeAllChildren() {
1207 		version(win32_widgets)
1208 		foreach(child; _children) {
1209 			child.removeAllChildren();
1210 			if(child.hwnd) {
1211 				DestroyWindow(child.hwnd);
1212 				child.hwnd = null;
1213 			}
1214 		}
1215 		auto orig = this._children;
1216 		this._children = null;
1217 		foreach(idx, w; orig)
1218 			this.widgetRemoved(idx, w);
1219 
1220 		queueRecomputeChildLayout();
1221 	}
1222 
1223 	/++
1224 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1225 	+/
1226 	@scriptable
1227 	Widget getChildByName(string name) {
1228 		return getByName(name);
1229 	}
1230 	/++
1231 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1232 	+/
1233 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1234 		if(this.name == name)
1235 			if(auto c = cast(WidgetClass) this)
1236 				return c;
1237 		foreach(child; children) {
1238 			auto w = child.getByName(name);
1239 			if(auto c = cast(WidgetClass) w)
1240 				return c;
1241 		}
1242 		return null;
1243 	}
1244 
1245 	/++
1246 		The name is a string tag that is used to reference the widget from scripts, gui loaders, declarative ui templates, etc. Similar to a HTML id attribute.
1247 		Names should be unique in a window.
1248 
1249 		See_Also: [getByName], [getChildByName]
1250 	+/
1251 	@scriptable string name;
1252 
1253 	private EventHandler[][string] bubblingEventHandlers;
1254 	private EventHandler[][string] capturingEventHandlers;
1255 
1256 	/++
1257 		Default event handlers. These are called on the appropriate
1258 		event unless [Event.preventDefault] is called on the event at
1259 		some point through the bubbling process.
1260 
1261 
1262 		If you are implementing your own widget and want to add custom
1263 		events, you should follow the same pattern here: create a virtual
1264 		function named `defaultEventHandler_eventname` with the implementation,
1265 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1266 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1267 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1268 		This ensures virtual dispatch based on the correct subclass.
1269 
1270 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1271 		overridden version.
1272 
1273 		You only need to do that on parent classes adding NEW event types. If you
1274 		just want to change the default behavior of an existing event type in a subclass,
1275 		you override the function (and optionally call `super.method_name`) like normal.
1276 
1277 	+/
1278 	protected EventHandler[string] defaultEventHandlers;
1279 
1280 	/// ditto
1281 	void setupDefaultEventHandlers() {
1282 		defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); };
1283 		defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); };
1284 		defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); };
1285 		defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); };
1286 		defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); };
1287 		defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); };
1288 		defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); };
1289 		defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); };
1290 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); };
1291 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); };
1292 		defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); };
1293 		defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); };
1294 		defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); };
1295 		defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); };
1296 		defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); };
1297 		defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); };
1298 		defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); };
1299 		defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); };
1300 	}
1301 
1302 	/// ditto
1303 	void defaultEventHandler_click(ClickEvent event) {}
1304 	/// ditto
1305 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1306 	/// ditto
1307 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1308 	/// ditto
1309 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1310 	/// ditto
1311 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1312 		if(event.button == MouseButton.left) {
1313 			if(this.tabStop) {
1314 				this.focus();
1315 			}
1316 		}
1317 	}
1318 	/// ditto
1319 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1320 	/// ditto
1321 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1322 	/// ditto
1323 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1324 	/// ditto
1325 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1326 	/// ditto
1327 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1328 	/// ditto
1329 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1330 	/// ditto
1331 	void defaultEventHandler_char(CharEvent event) {}
1332 	/// ditto
1333 	void defaultEventHandler_triggered(Event event) {}
1334 	/// ditto
1335 	void defaultEventHandler_change(Event event) {}
1336 	/// ditto
1337 	void defaultEventHandler_focus(Event event) {}
1338 	/// ditto
1339 	void defaultEventHandler_blur(Event event) {}
1340 	/// ditto
1341 	void defaultEventHandler_focusin(Event event) {}
1342 	/// ditto
1343 	void defaultEventHandler_focusout(Event event) {}
1344 
1345 	/++
1346 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1347 
1348 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1349 
1350 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1351 		of participating in handler delegation.
1352 
1353 		$(TIP
1354 			Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need.
1355 		)
1356 	+/
1357 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1358 		return addEventListener(event, (Widget, scope Event e) {
1359 			if(e.srcElement is this)
1360 				handler();
1361 		}, useCapture);
1362 	}
1363 
1364 	/// ditto
1365 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1366 		return addEventListener(event, (Widget, Event e) {
1367 			if(e.srcElement is this)
1368 				handler(e);
1369 		}, useCapture);
1370 	}
1371 
1372 	/// ditto
1373 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1374 		static if(is(Handler Fn == delegate)) {
1375 		static if(is(Fn Params == __parameters)) {
1376 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1377 				if(e.srcElement !is this)
1378 					return;
1379 				auto ty = cast(Params[0]) e;
1380 				if(ty !is null)
1381 					handler(ty);
1382 			}, useCapture);
1383 		} else static assert(0);
1384 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1385 	}
1386 
1387 	/// ditto
1388 	@scriptable
1389 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1390 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1391 	}
1392 
1393 	/// ditto
1394 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1395 		static if(is(Handler Fn == delegate)) {
1396 		static if(is(Fn Params == __parameters)) {
1397 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1398 				auto ty = cast(Params[0]) e;
1399 				if(ty !is null)
1400 					handler(ty);
1401 			}, useCapture);
1402 		} else static assert(0);
1403 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1404 	}
1405 
1406 	/// ditto
1407 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1408 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1409 	}
1410 
1411 	/// ditto
1412 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1413 		if(event.length > 2 && event[0..2] == "on")
1414 			event = event[2 .. $];
1415 
1416 		if(useCapture)
1417 			capturingEventHandlers[event] ~= handler;
1418 		else
1419 			bubblingEventHandlers[event] ~= handler;
1420 
1421 		return EventListener(this, event, handler, useCapture);
1422 	}
1423 
1424 	/// ditto
1425 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1426 		if(event.length > 2 && event[0..2] == "on")
1427 			event = event[2 .. $];
1428 
1429 		if(useCapture) {
1430 			if(event in capturingEventHandlers)
1431 			foreach(ref evt; capturingEventHandlers[event])
1432 				if(evt is handler) evt = null;
1433 		} else {
1434 			if(event in bubblingEventHandlers)
1435 			foreach(ref evt; bubblingEventHandlers[event])
1436 				if(evt is handler) evt = null;
1437 		}
1438 	}
1439 
1440 	/// ditto
1441 	void removeEventListener(EventListener listener) {
1442 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1443 	}
1444 
1445 	static if(UsingSimpledisplayX11) {
1446 		void discardXConnectionState() {
1447 			foreach(child; children)
1448 				child.discardXConnectionState();
1449 		}
1450 
1451 		void recreateXConnectionState() {
1452 			foreach(child; children)
1453 				child.recreateXConnectionState();
1454 			redraw();
1455 		}
1456 	}
1457 
1458 	/++
1459 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1460 
1461 		History:
1462 			`globalCoordinates` was made `final` on May 11, 2021.
1463 	+/
1464 	Point globalCoordinates() {
1465 		int x = this.x;
1466 		int y = this.y;
1467 		auto p = this.parent;
1468 		while(p) {
1469 			x += p.x;
1470 			y += p.y;
1471 			p = p.parent;
1472 		}
1473 
1474 		static if(UsingSimpledisplayX11) {
1475 			auto dpy = XDisplayConnection.get;
1476 			arsd.simpledisplay.Window dummyw;
1477 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1478 		} else version(Windows) {
1479 			POINT pt;
1480 			pt.x = x;
1481 			pt.y = y;
1482 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1483 			x = pt.x;
1484 			y = pt.y;
1485 		} else {
1486 			featureNotImplemented();
1487 		}
1488 
1489 		return Point(x, y);
1490 	}
1491 
1492 	version(win32_widgets)
1493 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1494 
1495 	version(win32_widgets)
1496 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1497 	void handleWmCommand(ushort cmd, ushort id) {}
1498 
1499 	version(win32_widgets)
1500 	/++
1501 		Called when a WM_NOTIFY is sent to the associated hwnd.
1502 
1503 		History:
1504 	+/
1505 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1506 
1507 	version(win32_widgets)
1508 	deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); }
1509 
1510 	/++
1511 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1512 
1513 		Updates to this variable will only be made visible on the next mouse enter event.
1514 	+/
1515 	@scriptable string statusTip;
1516 	// string toolTip;
1517 	// string helpText;
1518 
1519 	/++
1520 		If true, this widget can be focused via keyboard control with the tab key.
1521 
1522 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1523 	+/
1524 	bool tabStop = true;
1525 	/++
1526 		The tab key cycles through widgets by the order of a.tabOrder < b.tabOrder. If they are equal, it does them in child order (which is typically the order they were added to the widget.)
1527 	+/
1528 	int tabOrder;
1529 
1530 	version(win32_widgets) {
1531 		static Widget[HWND] nativeMapping;
1532 		/// The native handle, if there is one.
1533 		HWND hwnd;
1534 		WNDPROC originalWindowProcedure;
1535 
1536 		SimpleWindow simpleWindowWrappingHwnd;
1537 
1538 		// please note it IGNORES your return value and does NOT forward it to Windows!
1539 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1540 			return 0;
1541 		}
1542 	}
1543 	private bool implicitlyCreated;
1544 
1545 	/// Child's position relative to the parent's origin. only the layout manager should be modifying this and even reading it is of limited utility. It may be made `private` at some point in the future without advance notice. Do NOT depend on it being available unless you are writing a layout manager.
1546 	int x;
1547 	/// ditto
1548 	int y;
1549 	private int _width;
1550 	private int _height;
1551 	private Widget[] _children;
1552 	private Widget _parent;
1553 	private Window _parentWindow;
1554 
1555 	/++
1556 		Returns the window to which this widget is attached.
1557 
1558 		History:
1559 			Prior to May 11, 2021, the `Window parentWindow` variable was directly available. Now, only this property getter is available and the actual store is private.
1560 	+/
1561 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1562 	private @property void parentWindow(Window parent) {
1563 		auto old = _parentWindow;
1564 		_parentWindow = parent;
1565 		newParentWindow(old, _parentWindow);
1566 		foreach(child; children)
1567 			child.parentWindow = parent; // please note that this is recursive
1568 	}
1569 
1570 	/++
1571 		Called when the widget has been added to or remove from a parent window.
1572 
1573 		Note that either oldParent and/or newParent may be null any time this is called.
1574 
1575 		History:
1576 			Added September 13, 2024
1577 	+/
1578 	protected void newParentWindow(Window oldParent, Window newParent) {}
1579 
1580 	/++
1581 		Returns the list of the widget's children.
1582 
1583 		History:
1584 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1585 
1586 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1587 	+/
1588 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1589 
1590 	/++
1591 		Returns the widget's parent.
1592 
1593 		History:
1594 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1595 
1596 			The parent should only be managed by the [addChild] and [removeWidget] method.
1597 	+/
1598 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1599 
1600 	/// The widget's current size.
1601 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1602 	/// ditto
1603 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1604 
1605 	/// Only the layout manager should be calling these.
1606 	final protected @property int width(int a) @safe { return _width = a; }
1607 	/// ditto
1608 	final protected @property int height(int a) @safe { return _height = a; }
1609 
1610 	/++
1611 		This function is called by the layout engine after it has updated the position (in variables `x` and `y`) and the size (in properties `width` and `height`) to give you a chance to update the actual position of the native child window (if there is one) or whatever.
1612 
1613 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
1614 	+/
1615 	protected void registerMovement() {
1616 		version(win32_widgets) {
1617 			if(hwnd) {
1618 				auto pos = getChildPositionRelativeToParentHwnd(this);
1619 				MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn
1620 				this.redraw();
1621 			}
1622 		}
1623 		sendResizeEvent();
1624 	}
1625 
1626 	/// Creates the widget and adds it to the parent.
1627 	this(Widget parent) {
1628 		if(parent !is null)
1629 			parent.addChild(this);
1630 		setupDefaultEventHandlers();
1631 	}
1632 
1633 	/// Returns true if this is the current focused widget inside the parent window. Please note it may return `true` when the window itself is unfocused. In that case, it indicates this widget will receive focuse again when the window does.
1634 	@scriptable
1635 	bool isFocused() {
1636 		return parentWindow && parentWindow.focusedWidget is this;
1637 	}
1638 
1639 	private bool showing_ = true;
1640 	///
1641 	bool showing() const { return showing_; }
1642 	///
1643 	bool hidden() const { return !showing_; }
1644 	/++
1645 		Shows or hides the window. Meant to be assigned as a property. If `recalculate` is true (the default), it recalculates the layout of the parent widget to use the space this widget being hidden frees up or make space for this widget to appear again.
1646 
1647 		Note that a widget only ever shows if all its parents are showing too.
1648 	+/
1649 	void showing(bool s, bool recalculate = true) {
1650 		if(s != showing_) {
1651 			showing_ = s;
1652 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
1653 
1654 			showNativeWindowChildren(s);
1655 
1656 			if(parent && recalculate) {
1657 				parent.queueRecomputeChildLayout();
1658 				parent.redraw();
1659 			}
1660 
1661 			if(s) {
1662 				queueRecomputeChildLayout();
1663 				redraw();
1664 			}
1665 		}
1666 	}
1667 	/// Convenience method for `showing = true`
1668 	@scriptable
1669 	void show() {
1670 		showing = true;
1671 	}
1672 	/// Convenience method for `showing = false`
1673 	@scriptable
1674 	void hide() {
1675 		showing = false;
1676 	}
1677 
1678 	/++
1679 		If you are a native window, show/hide it based on shouldShow and return `true`.
1680 
1681 		Otherwise, do nothing and return false.
1682 	+/
1683 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
1684 		version(win32_widgets) {
1685 			if(hwnd) {
1686 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
1687 				return true;
1688 			} else {
1689 				return false;
1690 			}
1691 		} else {
1692 			return false;
1693 		}
1694 	}
1695 
1696 	private void showNativeWindowChildren(bool s) {
1697 		if(!showOrHideIfNativeWindow(s && showing))
1698 			foreach(child; children)
1699 				child.showNativeWindowChildren(s);
1700 	}
1701 
1702 	///
1703 	@scriptable
1704 	void focus() {
1705 		assert(parentWindow !is null);
1706 		if(isFocused())
1707 			return;
1708 
1709 		if(parentWindow.focusedWidget) {
1710 			// FIXME: more details here? like from and to
1711 			auto from = parentWindow.focusedWidget;
1712 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
1713 			parentWindow.focusedWidget = null;
1714 			from.emit!BlurEvent();
1715 			this.emit!FocusOutEvent();
1716 		}
1717 
1718 
1719 		version(win32_widgets) {
1720 			if(this.hwnd !is null)
1721 				SetFocus(this.hwnd);
1722 		}
1723 		//else static if(UsingSimpledisplayX11)
1724 			//this.parentWindow.win.focus();
1725 
1726 		parentWindow.focusedWidget = this;
1727 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
1728 		this.emit!FocusEvent();
1729 		this.emit!FocusInEvent();
1730 	}
1731 
1732 	/+
1733 	/++
1734 		Unfocuses the widget. This may reset
1735 	+/
1736 	@scriptable
1737 	void blur() {
1738 
1739 	}
1740 	+/
1741 
1742 
1743 	/++
1744 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
1745 
1746 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
1747 	+/
1748 	void attachedToWindow(Window w) {}
1749 	/++
1750 		Callback when the widget is added to another widget.
1751 
1752 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
1753 	+/
1754 	void addedTo(Widget w) {}
1755 
1756 	/++
1757 		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.
1758 
1759 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
1760 	+/
1761 	protected void addChild(Widget w, int position = int.max) {
1762 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
1763 		assert(w !is this, "Child cannot be its own parent!");
1764 		w._parent = this;
1765 		if(position == int.max || position == children.length) {
1766 			_children ~= w;
1767 		} else {
1768 			assert(position < _children.length);
1769 			_children.length = _children.length + 1;
1770 			for(int i = cast(int) _children.length - 1; i > position; i--)
1771 				_children[i] = _children[i - 1];
1772 			_children[position] = w;
1773 		}
1774 
1775 		this.parentWindow = this._parentWindow;
1776 
1777 		w.addedTo(this);
1778 
1779 		bool parentIsNative;
1780 		version(win32_widgets) {
1781 			parentIsNative = hwnd !is null;
1782 		}
1783 		if(!parentIsNative && !showing)
1784 			w.showOrHideIfNativeWindow(false);
1785 
1786 		if(parentWindow !is null) {
1787 			w.attachedToWindow(parentWindow);
1788 			parentWindow.queueRecomputeChildLayout();
1789 			parentWindow.redraw();
1790 		}
1791 	}
1792 
1793 	/++
1794 		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.
1795 	+/
1796 	Widget getChildAtPosition(int x, int y) {
1797 		// it goes backward so the last one to show gets picked first
1798 		// might use z-index later
1799 		foreach_reverse(child; children) {
1800 			if(child.hidden)
1801 				continue;
1802 			if(child.x <= x && child.y <= y
1803 				&& ((x - child.x) < child.width)
1804 				&& ((y - child.y) < child.height))
1805 			{
1806 				return child;
1807 			}
1808 		}
1809 
1810 		return null;
1811 	}
1812 
1813 	/++
1814 		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.
1815 
1816 		History:
1817 			Added July 2, 2021 (v10.2)
1818 	+/
1819 	protected void addScrollPosition(ref int x, ref int y) {};
1820 
1821 	/++
1822 		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.
1823 
1824 		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.
1825 
1826 		[paint] is not called for system widgets as the OS library draws them instead.
1827 
1828 
1829 		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.
1830 
1831 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
1832 
1833 		History:
1834 			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.
1835 	+/
1836 	void paint(WidgetPainter painter) {
1837 		version(win32_widgets)
1838 			if(hwnd) {
1839 				return;
1840 			}
1841 		painter.drawThemed(&paintContent); // note this refers to the following overload
1842 	}
1843 
1844 	/++
1845 		Responsible for drawing the content as the theme engine is responsible for other elements.
1846 
1847 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
1848 
1849 		Params:
1850 			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.
1851 
1852 			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.
1853 
1854 			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.
1855 
1856 		Returns:
1857 			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.
1858 
1859 		History:
1860 			Added May 15, 2021
1861 	+/
1862 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
1863 		return bounds;
1864 	}
1865 
1866 	deprecated("Change ScreenPainter to WidgetPainter")
1867 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
1868 
1869 	/// I don't actually like the name of this
1870 	/// this draws a background on it
1871 	void erase(WidgetPainter painter) {
1872 		version(win32_widgets)
1873 			if(hwnd) return; // Windows will do it. I think.
1874 
1875 		auto c = getComputedStyle().background.color;
1876 		painter.fillColor = c;
1877 		painter.outlineColor = c;
1878 
1879 		version(win32_widgets) {
1880 			HANDLE b, p;
1881 			if(c.a == 0 && parent is parentWindow) {
1882 				// I don't remember why I had this really...
1883 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
1884 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
1885 			}
1886 		}
1887 		painter.drawRectangle(Point(0, 0), width, height);
1888 		version(win32_widgets) {
1889 			if(c.a == 0 && parent is parentWindow) {
1890 				SelectObject(painter.impl.hdc, p);
1891 				SelectObject(painter.impl.hdc, b);
1892 			}
1893 		}
1894 	}
1895 
1896 	///
1897 	WidgetPainter draw() {
1898 		int x = this.x, y = this.y;
1899 		auto parent = this.parent;
1900 		while(parent) {
1901 			x += parent.x;
1902 			y += parent.y;
1903 			parent = parent.parent;
1904 		}
1905 
1906 		auto painter = parentWindow.win.draw(true);
1907 		painter.originX = x;
1908 		painter.originY = y;
1909 		painter.setClipRectangle(Point(0, 0), width, height);
1910 		return WidgetPainter(painter, this);
1911 	}
1912 
1913 	/// 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.
1914 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
1915 		if(hidden)
1916 			return;
1917 
1918 		int paintX = x;
1919 		int paintY = y;
1920 		if(this.useNativeDrawing()) {
1921 			paintX = 0;
1922 			paintY = 0;
1923 			lox = 0;
1924 			loy = 0;
1925 			containment = Rectangle(0, 0, int.max, int.max);
1926 		}
1927 
1928 		painter.originX = lox + paintX;
1929 		painter.originY = loy + paintY;
1930 
1931 		bool actuallyPainted = false;
1932 
1933 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
1934 		if(clip == Rectangle.init) {
1935 			// writeln(this, " clipped out");
1936 			return;
1937 		}
1938 
1939 		bool invalidateChildren = invalidate;
1940 
1941 		if(redrawRequested || force) {
1942 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
1943 
1944 			painter.drawingUpon = this;
1945 
1946 			erase(painter);
1947 			if(painter.visualTheme)
1948 				painter.visualTheme.doPaint(this, painter);
1949 			else
1950 				paint(painter);
1951 
1952 			if(invalidate) {
1953 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
1954 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
1955 				painter.invalidateRect(region);
1956 				// children are contained inside this, so no need to do extra work
1957 				invalidateChildren = false;
1958 			}
1959 
1960 			redrawRequested = false;
1961 			actuallyPainted = true;
1962 		}
1963 
1964 		foreach(child; children) {
1965 			version(win32_widgets)
1966 				if(child.useNativeDrawing()) continue;
1967 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
1968 		}
1969 
1970 		version(win32_widgets)
1971 		foreach(child; children) {
1972 			if(child.useNativeDrawing) {
1973 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
1974 				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
1975 			}
1976 		}
1977 	}
1978 
1979 	protected bool useNativeDrawing() nothrow {
1980 		version(win32_widgets)
1981 			return hwnd !is null;
1982 		else
1983 			return false;
1984 	}
1985 
1986 	private static class RedrawEvent {}
1987 	private __gshared re = new RedrawEvent();
1988 
1989 	private bool redrawRequested;
1990 	///
1991 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
1992 		redrawRequested = true;
1993 
1994 		if(this.parentWindow) {
1995 			auto sw = this.parentWindow.win;
1996 			assert(sw !is null);
1997 			if(!sw.eventQueued!RedrawEvent) {
1998 				sw.postEvent(re);
1999 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2000 			}
2001 		}
2002 	}
2003 
2004 	private SimpleWindow drawableWindow;
2005 
2006 	/++
2007 		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.
2008 
2009 		Returns:
2010 			`true` if you should do your default behavior.
2011 
2012 		History:
2013 			Added May 5, 2021
2014 
2015 		Bugs:
2016 			It does not do the static checks on gdc right now.
2017 	+/
2018 	final protected bool emit(EventType, this This, Args...)(Args args) {
2019 		version(GNU) {} else
2020 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2021 		auto e = new EventType(this, args);
2022 		e.dispatch();
2023 		return !e.defaultPrevented;
2024 	}
2025 	/// ditto
2026 	final protected bool emit(string eventString, this This)() {
2027 		auto e = new Event(eventString, this);
2028 		e.dispatch();
2029 		return !e.defaultPrevented;
2030 	}
2031 
2032 	/++
2033 		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.
2034 
2035 		History:
2036 			Added May 5, 2021
2037 	+/
2038 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2039 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2040 		return addEventListener(handler);
2041 	}
2042 
2043 	/++
2044 		Gets the computed style properties from the visual theme.
2045 
2046 		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].)
2047 
2048 		History:
2049 			Added May 8, 2021
2050 	+/
2051 	final StyleInformation getComputedStyle() {
2052 		return StyleInformation(this);
2053 	}
2054 
2055 	int focusableWidgets(scope int delegate(Widget) dg) {
2056 		foreach(widget; WidgetStream(this)) {
2057 			if(widget.tabStop && !widget.hidden) {
2058 				int result = dg(widget);
2059 				if (result)
2060 					return result;
2061 			}
2062 		}
2063 		return 0;
2064 	}
2065 
2066 	/++
2067 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2068 		for the given content box (the area between the padding)
2069 
2070 		History:
2071 			Added January 4, 2023 (dub v11.0)
2072 	+/
2073 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2074 		auto cs = getComputedStyle();
2075 
2076 		auto borderWidth = getBorderWidth(cs.borderStyle);
2077 
2078 		auto rect = contentBox;
2079 
2080 		rect.left -= borderWidth;
2081 		rect.right += borderWidth;
2082 		rect.top -= borderWidth;
2083 		rect.bottom += borderWidth;
2084 
2085 		auto insideBorderRect = rect;
2086 
2087 		rect.left -= cs.paddingLeft;
2088 		rect.right += cs.paddingRight;
2089 		rect.top -= cs.paddingTop;
2090 		rect.bottom += cs.paddingBottom;
2091 
2092 		return rect;
2093 	}
2094 
2095 
2096 	// FIXME: I kinda want to hide events from implementation widgets
2097 	// so it just catches them all and stops propagation...
2098 	// i guess i can do it with a event listener on star.
2099 
2100 	mixin Emits!KeyDownEvent; ///
2101 	mixin Emits!KeyUpEvent; ///
2102 	mixin Emits!CharEvent; ///
2103 
2104 	mixin Emits!MouseDownEvent; ///
2105 	mixin Emits!MouseUpEvent; ///
2106 	mixin Emits!ClickEvent; ///
2107 	mixin Emits!DoubleClickEvent; ///
2108 	mixin Emits!MouseMoveEvent; ///
2109 	mixin Emits!MouseOverEvent; ///
2110 	mixin Emits!MouseOutEvent; ///
2111 	mixin Emits!MouseEnterEvent; ///
2112 	mixin Emits!MouseLeaveEvent; ///
2113 
2114 	mixin Emits!ResizeEvent; ///
2115 
2116 	mixin Emits!BlurEvent; ///
2117 	mixin Emits!FocusEvent; ///
2118 
2119 	mixin Emits!FocusInEvent; ///
2120 	mixin Emits!FocusOutEvent; ///
2121 }
2122 
2123 /+
2124 /++
2125 	Interface to indicate that the widget has a simple value property.
2126 
2127 	History:
2128 		Added August 26, 2021
2129 +/
2130 interface HasValue!T {
2131 	/// Getter
2132 	@property T value();
2133 	/// Setter
2134 	@property void value(T);
2135 }
2136 
2137 /++
2138 	Interface to indicate that the widget has a range of possible values for its simple value property.
2139 	This would be present on something like a slider or possibly a number picker.
2140 
2141 	History:
2142 		Added September 11, 2021
2143 +/
2144 interface HasRangeOfValues!T : HasValue!T {
2145 	/// The minimum and maximum values in the range, inclusive.
2146 	@property T minValue();
2147 	@property void minValue(T); /// ditto
2148 	@property T maxValue(); /// ditto
2149 	@property void maxValue(T); /// ditto
2150 
2151 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2152 	@property void step(T);
2153 	@property T step(); /// ditto
2154 }
2155 
2156 /++
2157 	Interface to indicate that the widget has a list of possible values the user can choose from.
2158 	This would be present on something like a drop-down selector.
2159 
2160 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2161 	combobox.
2162 
2163 	History:
2164 		Added September 11, 2021
2165 +/
2166 interface HasListOfValues!T : HasValue!T {
2167 	@property T[] values;
2168 	@property void values(T[]);
2169 
2170 	@property int selectedIndex(); // note it may return -1!
2171 	@property void selectedIndex(int);
2172 }
2173 +/
2174 
2175 /++
2176 	History:
2177 		Added September 2021 (dub v10.4)
2178 +/
2179 class GridLayout : Layout {
2180 
2181 	// 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.
2182 
2183 	/++
2184 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2185 	+/
2186 	enum Gravity {
2187 		Center    = 0,
2188 		NorthWest = North | West,
2189 		North     = 0b10_00,
2190 		NorthEast = North | East,
2191 		West      = 0b00_10,
2192 		East      = 0b00_01,
2193 		SouthWest = South | West,
2194 		South     = 0b01_00,
2195 		SouthEast = South | East,
2196 	}
2197 
2198 	/++
2199 		The width and height are in some proportional units and can often just be 12.
2200 	+/
2201 	this(int width, int height, Widget parent) {
2202 		this.gridWidth = width;
2203 		this.gridHeight = height;
2204 		super(parent);
2205 	}
2206 
2207 	/++
2208 		Sets the position of the given child.
2209 
2210 		The units of these arguments are in the proportional grid units you set in the constructor.
2211 	+/
2212 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2213 		// ensure it is in bounds
2214 		// then ensure no overlaps
2215 
2216 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2217 
2218 		foreach(ref position; positions) {
2219 			if(position.widget is child) {
2220 				position = p;
2221 				goto set;
2222 			}
2223 		}
2224 
2225 		positions ~= p;
2226 
2227 		set:
2228 
2229 		// FIXME: should this batch?
2230 		queueRecomputeChildLayout();
2231 
2232 		return child;
2233 	}
2234 
2235 	override void addChild(Widget w, int position = int.max) {
2236 		super.addChild(w, position);
2237 		//positions ~= ChildPosition(w);
2238 		if(position != int.max) {
2239 			// FIXME: align it so they actually match.
2240 		}
2241 	}
2242 
2243 	override void widgetRemoved(size_t idx, Widget w) {
2244 		// FIXME: keep the positions array aligned
2245 		// positions[idx].widget = null;
2246 	}
2247 
2248 	override void recomputeChildLayout() {
2249 		registerMovement();
2250 		int onGrid = cast(int) positions.length;
2251 		c: foreach(child; children) {
2252 			// just snap it to the grid
2253 			if(onGrid)
2254 			foreach(position; positions)
2255 				if(position.widget is child) {
2256 					child.x = this.width * position.x / this.gridWidth;
2257 					child.y = this.height * position.y / this.gridHeight;
2258 					child.width = this.width * position.width / this.gridWidth;
2259 					child.height = this.height * position.height / this.gridHeight;
2260 
2261 					auto diff = child.width - child.maxWidth();
2262 					// FIXME: gravity?
2263 					if(diff > 0) {
2264 						child.width = child.width - diff;
2265 
2266 						if(position.gravity & Gravity.West) {
2267 							// nothing needed, already aligned
2268 						} else if(position.gravity & Gravity.East) {
2269 							child.x += diff;
2270 						} else {
2271 							child.x += diff / 2;
2272 						}
2273 					}
2274 
2275 					diff = child.height - child.maxHeight();
2276 					// FIXME: gravity?
2277 					if(diff > 0) {
2278 						child.height = child.height - diff;
2279 
2280 						if(position.gravity & Gravity.North) {
2281 							// nothing needed, already aligned
2282 						} else if(position.gravity & Gravity.South) {
2283 							child.y += diff;
2284 						} else {
2285 							child.y += diff / 2;
2286 						}
2287 					}
2288 
2289 
2290 					child.recomputeChildLayout();
2291 					onGrid--;
2292 					continue c;
2293 				}
2294 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2295 		}
2296 	}
2297 
2298 	private struct ChildPosition {
2299 		Widget widget;
2300 		int x;
2301 		int y;
2302 		int width;
2303 		int height;
2304 		Gravity gravity;
2305 	}
2306 	private ChildPosition[] positions;
2307 
2308 	int gridWidth = 12;
2309 	int gridHeight = 12;
2310 }
2311 
2312 ///
2313 abstract class ComboboxBase : Widget {
2314 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2315 	// or to always show the list, we want CBS_SIMPLE == 1
2316 	version(win32_widgets)
2317 		this(uint style, Widget parent) {
2318 			super(parent);
2319 			createWin32Window(this, "ComboBox"w, null, style);
2320 		}
2321 	else version(custom_widgets)
2322 		this(Widget parent) {
2323 			super(parent);
2324 
2325 			addEventListener((KeyDownEvent event) {
2326 				if(event.key == Key.Up) {
2327 					if(selection_ > -1) { // -1 means select blank
2328 						selection_--;
2329 						fireChangeEvent();
2330 					}
2331 					event.preventDefault();
2332 				}
2333 				if(event.key == Key.Down) {
2334 					if(selection_ + 1 < options.length) {
2335 						selection_++;
2336 						fireChangeEvent();
2337 					}
2338 					event.preventDefault();
2339 				}
2340 
2341 			});
2342 
2343 		}
2344 	else static assert(false);
2345 
2346 	/++
2347 		Returns the current list of options in the selection.
2348 
2349 		History:
2350 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2351 	+/
2352 	final @property string[] options() const {
2353 		return cast(string[]) options_;
2354 	}
2355 
2356 	private string[] options_;
2357 	private int selection_ = -1;
2358 
2359 	/++
2360 		Adds an option to the end of options array.
2361 	+/
2362 	void addOption(string s) {
2363 		options_ ~= s;
2364 		version(win32_widgets)
2365 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2366 	}
2367 
2368 	/++
2369 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2370 	+/
2371 	int getSelection() {
2372 		return selection_;
2373 	}
2374 
2375 	/++
2376 		Returns the current selection as a string.
2377 
2378 		History:
2379 			Added November 17, 2021
2380 	+/
2381 	string getSelectionString() {
2382 		return selection_ == -1 ? null : options[selection_];
2383 	}
2384 
2385 	/++
2386 		Sets the current selection to an index in the options array, or to the given option if present.
2387 		Please note that the string version may do a linear lookup.
2388 
2389 		Returns:
2390 			the index you passed in
2391 
2392 		History:
2393 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2394 
2395 			The return value was `void` prior to March 1, 2022.
2396 	+/
2397 	int setSelection(int idx) {
2398 		selection_ = idx;
2399 		version(win32_widgets)
2400 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2401 
2402 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2403 		t.dispatch();
2404 
2405 		return idx;
2406 	}
2407 
2408 	/// ditto
2409 	int setSelection(string s) {
2410 		if(s !is null)
2411 		foreach(idx, item; options)
2412 			if(item == s) {
2413 				return setSelection(cast(int) idx);
2414 			}
2415 		return setSelection(-1);
2416 	}
2417 
2418 	/++
2419 		This event is fired when the selection changes. Note it inherits
2420 		from ChangeEvent!string, meaning you can use that as well, and it also
2421 		fills in [Event.intValue].
2422 	+/
2423 	static class SelectionChangedEvent : ChangeEvent!string {
2424 		this(Widget target, int iv, string sv) {
2425 			super(target, &stringValue);
2426 			this.iv = iv;
2427 			this.sv = sv;
2428 		}
2429 		immutable int iv;
2430 		immutable string sv;
2431 
2432 		override @property string stringValue() { return sv; }
2433 		override @property int intValue() { return iv; }
2434 	}
2435 
2436 	version(win32_widgets)
2437 	override void handleWmCommand(ushort cmd, ushort id) {
2438 		if(cmd == CBN_SELCHANGE) {
2439 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2440 			fireChangeEvent();
2441 		}
2442 	}
2443 
2444 	private void fireChangeEvent() {
2445 		if(selection_ >= options.length)
2446 			selection_ = -1;
2447 
2448 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2449 		t.dispatch();
2450 	}
2451 
2452 	version(win32_widgets) {
2453 		override int minHeight() { return defaultLineHeight + 6; }
2454 		override int maxHeight() { return defaultLineHeight + 6; }
2455 	} else {
2456 		override int minHeight() { return defaultLineHeight + 4; }
2457 		override int maxHeight() { return defaultLineHeight + 4; }
2458 	}
2459 
2460 	version(custom_widgets) {
2461 
2462 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2463 
2464 		SimpleWindow dropDown;
2465 		void popup() {
2466 			auto w = width;
2467 			// FIXME: suggestedDropdownHeight see below
2468 			auto h = cast(int) this.options.length * defaultLineHeight + 8;
2469 
2470 			auto coord = this.globalCoordinates();
2471 			auto dropDown = new SimpleWindow(
2472 				w, h,
2473 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null);
2474 
2475 			dropDown.move(coord.x, coord.y + this.height);
2476 
2477 			{
2478 				auto cs = getComputedStyle();
2479 				auto painter = dropDown.draw();
2480 				draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2481 				auto p = Point(4, 4);
2482 				painter.outlineColor = cs.foregroundColor;
2483 				foreach(option; options) {
2484 					painter.drawText(p, option);
2485 					p.y += defaultLineHeight;
2486 				}
2487 			}
2488 
2489 			dropDown.setEventHandlers(
2490 				(MouseEvent event) {
2491 					if(event.type == MouseEventType.buttonReleased) {
2492 						dropDown.close();
2493 						auto element = (event.y - 4) / defaultLineHeight;
2494 						if(element >= 0 && element <= options.length) {
2495 							selection_ = element;
2496 
2497 							fireChangeEvent();
2498 						}
2499 					}
2500 				}
2501 			);
2502 
2503 			dropDown.visibilityChanged = (bool visible) {
2504 				if(visible) {
2505 					this.redraw();
2506 					dropDown.grabInput();
2507 				} else {
2508 					dropDown.releaseInputGrab();
2509 				}
2510 			};
2511 
2512 			dropDown.show();
2513 		}
2514 
2515 	}
2516 }
2517 
2518 /++
2519 	A drop-down list where the user must select one of the
2520 	given options. Like `<select>` in HTML.
2521 +/
2522 class DropDownSelection : ComboboxBase {
2523 	this(Widget parent) {
2524 		version(win32_widgets)
2525 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
2526 		else version(custom_widgets) {
2527 			super(parent);
2528 
2529 			addEventListener("focus", () { this.redraw; });
2530 			addEventListener("blur", () { this.redraw; });
2531 			addEventListener(EventType.change, () { this.redraw; });
2532 			addEventListener("mousedown", () { this.focus(); this.popup(); });
2533 			addEventListener((KeyDownEvent event) {
2534 				if(event.key == Key.Space)
2535 					popup();
2536 			});
2537 		} else static assert(false);
2538 	}
2539 
2540 	mixin Padding!q{2};
2541 	static class Style : Widget.Style {
2542 		override FrameStyle borderStyle() { return FrameStyle.risen; }
2543 	}
2544 	mixin OverrideStyle!Style;
2545 
2546 	version(custom_widgets)
2547 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2548 		auto cs = getComputedStyle();
2549 
2550 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
2551 
2552 		painter.outlineColor = cs.foregroundColor;
2553 		painter.fillColor = cs.foregroundColor;
2554 
2555 		/+
2556 		Point[4] triangle;
2557 		enum padding = 6;
2558 		enum paddingV = 7;
2559 		enum triangleWidth = 10;
2560 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
2561 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
2562 		triangle[2] = Point(width - padding - 0, paddingV);
2563 		triangle[3] = triangle[0];
2564 		painter.drawPolygon(triangle[]);
2565 		+/
2566 
2567 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
2568 
2569 		painter.drawPolygon(
2570 			scaleWithDpi(Point(2, 6) + offset),
2571 			scaleWithDpi(Point(7, 11) + offset),
2572 			scaleWithDpi(Point(12, 6) + offset),
2573 			scaleWithDpi(Point(2, 6) + offset)
2574 		);
2575 
2576 
2577 		return bounds;
2578 	}
2579 
2580 	version(win32_widgets)
2581 	override void registerMovement() {
2582 		version(win32_widgets) {
2583 			if(hwnd) {
2584 				auto pos = getChildPositionRelativeToParentHwnd(this);
2585 				// the height given to this from Windows' perspective is supposed
2586 				// to include the drop down's height. so I add to it to give some
2587 				// room for that.
2588 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
2589 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
2590 			}
2591 		}
2592 		sendResizeEvent();
2593 	}
2594 }
2595 
2596 /++
2597 	A text box with a drop down arrow listing selections.
2598 	The user can choose from the list, or type their own.
2599 +/
2600 class FreeEntrySelection : ComboboxBase {
2601 	this(Widget parent) {
2602 		version(win32_widgets)
2603 			super(2 /* CBS_DROPDOWN */, parent);
2604 		else version(custom_widgets) {
2605 			super(parent);
2606 			auto hl = new HorizontalLayout(this);
2607 			lineEdit = new LineEdit(hl);
2608 
2609 			tabStop = false;
2610 
2611 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2612 
2613 			auto btn = new class ArrowButton {
2614 				this() {
2615 					super(ArrowDirection.down, hl);
2616 				}
2617 				override int maxHeight() {
2618 					return lineEdit.maxHeight;
2619 				}
2620 			};
2621 			//btn.addDirectEventListener("focus", &lineEdit.focus);
2622 			btn.addEventListener("triggered", &this.popup);
2623 			addEventListener(EventType.change, (Event event) {
2624 				lineEdit.content = event.stringValue;
2625 				lineEdit.focus();
2626 				redraw();
2627 			});
2628 		}
2629 		else static assert(false);
2630 	}
2631 
2632 	version(custom_widgets) {
2633 		LineEdit lineEdit;
2634 	}
2635 }
2636 
2637 /++
2638 	A combination of free entry with a list below it.
2639 +/
2640 class ComboBox : ComboboxBase {
2641 	this(Widget parent) {
2642 		version(win32_widgets)
2643 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
2644 		else version(custom_widgets) {
2645 			super(parent);
2646 			lineEdit = new LineEdit(this);
2647 			listWidget = new ListWidget(this);
2648 			listWidget.multiSelect = false;
2649 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
2650 				string c = null;
2651 				foreach(option; listWidget.options)
2652 					if(option.selected) {
2653 						c = option.label;
2654 						break;
2655 					}
2656 				lineEdit.content = c;
2657 			});
2658 
2659 			listWidget.tabStop = false;
2660 			this.tabStop = false;
2661 			listWidget.addEventListener("focus", &lineEdit.focus);
2662 			this.addEventListener("focus", &lineEdit.focus);
2663 
2664 			addDirectEventListener(EventType.change, {
2665 				listWidget.setSelection(selection_);
2666 				if(selection_ != -1)
2667 					lineEdit.content = options[selection_];
2668 				lineEdit.focus();
2669 				redraw();
2670 			});
2671 
2672 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2673 
2674 			listWidget.addDirectEventListener(EventType.change, {
2675 				int set = -1;
2676 				foreach(idx, opt; listWidget.options)
2677 					if(opt.selected) {
2678 						set = cast(int) idx;
2679 						break;
2680 					}
2681 				if(set != selection_)
2682 					this.setSelection(set);
2683 			});
2684 		} else static assert(false);
2685 	}
2686 
2687 	override int minHeight() { return defaultLineHeight * 3; }
2688 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
2689 	override int heightStretchiness() { return 5; }
2690 
2691 	version(custom_widgets) {
2692 		LineEdit lineEdit;
2693 		ListWidget listWidget;
2694 
2695 		override void addOption(string s) {
2696 			listWidget.options ~= ListWidget.Option(s);
2697 			ComboboxBase.addOption(s);
2698 		}
2699 	}
2700 }
2701 
2702 /+
2703 class Spinner : Widget {
2704 	version(win32_widgets)
2705 	this(Widget parent) {
2706 		super(parent);
2707 		parentWindow = parent.parentWindow;
2708 		auto hlayout = new HorizontalLayout(this);
2709 		lineEdit = new LineEdit(hlayout);
2710 		upDownControl = new UpDownControl(hlayout);
2711 	}
2712 
2713 	LineEdit lineEdit;
2714 	UpDownControl upDownControl;
2715 }
2716 
2717 class UpDownControl : Widget {
2718 	version(win32_widgets)
2719 	this(Widget parent) {
2720 		super(parent);
2721 		parentWindow = parent.parentWindow;
2722 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
2723 	}
2724 
2725 	override int minHeight() { return defaultLineHeight; }
2726 	override int maxHeight() { return defaultLineHeight * 3/2; }
2727 
2728 	override int minWidth() { return defaultLineHeight * 3/2; }
2729 	override int maxWidth() { return defaultLineHeight * 3/2; }
2730 }
2731 +/
2732 
2733 /+
2734 class DataView : Widget {
2735 	// this is the omnibus data viewer
2736 	// the internal data layout is something like:
2737 	// string[string][] but also each node can have parents
2738 }
2739 +/
2740 
2741 
2742 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
2743 
2744 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
2745 
2746 // FIXME: menus should prolly capture the mouse. ugh i kno.
2747 /*
2748 	TextEdit needs:
2749 
2750 	* caret manipulation
2751 	* selection control
2752 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
2753 
2754 	For example:
2755 
2756 	connect(paste, &textEdit.insertTextAtCaret);
2757 
2758 	would be nice.
2759 
2760 
2761 
2762 	I kinda want an omnibus dataview that combines list, tree,
2763 	and table - it can be switched dynamically between them.
2764 
2765 	Flattening policy: only show top level, show recursive, show grouped
2766 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
2767 
2768 	Single select, multi select, organization, drag+drop
2769 */
2770 
2771 //static if(UsingSimpledisplayX11)
2772 version(win32_widgets) {}
2773 else version(custom_widgets) {
2774 	enum scrollClickRepeatInterval = 50;
2775 
2776 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
2777 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
2778 	enum activeTabColor = lightAccentColor;
2779 	enum hoveringColor = Color(228, 228, 228);
2780 	enum buttonColor = windowBackgroundColor;
2781 	enum depressedButtonColor = darkAccentColor;
2782 	enum activeListXorColor = Color(255, 255, 127);
2783 	enum progressBarColor = Color(0, 0, 128);
2784 	enum activeMenuItemColor = Color(0, 0, 128);
2785 
2786 }}
2787 else static assert(false);
2788 deprecated("Get these properties off the `visualTheme` instead.") {
2789 	// these are used by horizontal rule so not just custom_widgets. for now at least.
2790 	enum darkAccentColor = Color(172, 172, 172);
2791 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
2792 }
2793 
2794 private const(wchar)* toWstringzInternal(in char[] s) {
2795 	wchar[] str;
2796 	str.reserve(s.length + 1);
2797 	foreach(dchar ch; s)
2798 		str ~= ch;
2799 	str ~= '\0';
2800 	return str.ptr;
2801 }
2802 
2803 static if(SimpledisplayTimerAvailable)
2804 void setClickRepeat(Widget w, int interval, int delay = 250) {
2805 	Timer timer;
2806 	int delayRemaining = delay / interval;
2807 	if(delayRemaining <= 1)
2808 		delayRemaining = 2;
2809 
2810 	immutable originalDelayRemaining = delayRemaining;
2811 
2812 	w.addDirectEventListener((scope MouseDownEvent ev) {
2813 		if(ev.srcElement !is w)
2814 			return;
2815 		if(timer !is null) {
2816 			timer.destroy();
2817 			timer = null;
2818 		}
2819 		delayRemaining = originalDelayRemaining;
2820 		timer = new Timer(interval, () {
2821 			if(delayRemaining > 0)
2822 				delayRemaining--;
2823 			else {
2824 				auto ev = new Event("triggered", w);
2825 				ev.sendDirectly();
2826 			}
2827 		});
2828 	});
2829 
2830 	w.addDirectEventListener((scope MouseUpEvent ev) {
2831 		if(ev.srcElement !is w)
2832 			return;
2833 		if(timer !is null) {
2834 			timer.destroy();
2835 			timer = null;
2836 		}
2837 	});
2838 
2839 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
2840 		if(ev.srcElement !is w)
2841 			return;
2842 		if(timer !is null) {
2843 			timer.destroy();
2844 			timer = null;
2845 		}
2846 	});
2847 
2848 }
2849 else
2850 void setClickRepeat(Widget w, int interval, int delay = 250) {}
2851 
2852 enum FrameStyle {
2853 	none, ///
2854 	risen, /// a 3d pop-out effect (think Windows 95 button)
2855 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
2856 	solid, ///
2857 	dotted, ///
2858 	fantasy, /// a style based on a popular fantasy video game
2859 	rounded, /// a rounded rectangle
2860 }
2861 
2862 version(custom_widgets)
2863 deprecated
2864 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
2865 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2866 }
2867 
2868 version(custom_widgets)
2869 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
2870 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
2871 }
2872 
2873 version(custom_widgets)
2874 deprecated
2875 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
2876 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2877 }
2878 
2879 int getBorderWidth(FrameStyle style) {
2880 	final switch(style) {
2881 		case FrameStyle.sunk, FrameStyle.risen:
2882 			return 2;
2883 		case FrameStyle.none:
2884 			return 0;
2885 		case FrameStyle.solid:
2886 			return 1;
2887 		case FrameStyle.dotted:
2888 			return 1;
2889 		case FrameStyle.fantasy:
2890 			return 3;
2891 		case FrameStyle.rounded:
2892 			return 2;
2893 	}
2894 }
2895 
2896 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
2897 	int borderWidth = getBorderWidth(style);
2898 	final switch(style) {
2899 		case FrameStyle.sunk, FrameStyle.risen:
2900 			// outer layer
2901 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
2902 		break;
2903 		case FrameStyle.none:
2904 			painter.outlineColor = background;
2905 		break;
2906 		case FrameStyle.solid:
2907 		case FrameStyle.rounded:
2908 			painter.pen = Pen(border, 1);
2909 		break;
2910 		case FrameStyle.dotted:
2911 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
2912 		break;
2913 		case FrameStyle.fantasy:
2914 			painter.pen = Pen(border, 3);
2915 		break;
2916 	}
2917 
2918 	painter.fillColor = background;
2919 
2920 	if(style == FrameStyle.rounded) {
2921 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
2922 	} else {
2923 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
2924 
2925 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
2926 			// 3d effect
2927 			auto vt = WidgetPainter.visualTheme;
2928 
2929 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
2930 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
2931 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
2932 
2933 			// inner layer
2934 			//right, bottom
2935 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
2936 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
2937 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
2938 			// left, top
2939 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
2940 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
2941 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
2942 		} else if(style == FrameStyle.fantasy) {
2943 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
2944 			painter.fillColor = Color.transparent;
2945 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
2946 		}
2947 	}
2948 
2949 	return borderWidth;
2950 }
2951 
2952 /++
2953 	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.
2954 
2955 	See_Also:
2956 		[MenuItem]
2957 		[ToolButton]
2958 		[Menu.addItem]
2959 +/
2960 class Action {
2961 	version(win32_widgets) {
2962 		private int id;
2963 		private static int lastId = 9000;
2964 		private static Action[int] mapping;
2965 	}
2966 
2967 	KeyEvent accelerator;
2968 
2969 	// FIXME: disable message
2970 	// and toggle thing?
2971 	// ??? and trigger arguments too ???
2972 
2973 	/++
2974 		Params:
2975 			label = the textual label
2976 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
2977 			triggered = initial handler, more can be added via the [triggered] member.
2978 	+/
2979 	this(string label, ushort icon = 0, void delegate() triggered = null) {
2980 		this.label = label;
2981 		this.iconId = icon;
2982 		if(triggered !is null)
2983 			this.triggered ~= triggered;
2984 		version(win32_widgets) {
2985 			id = ++lastId;
2986 			mapping[id] = this;
2987 		}
2988 	}
2989 
2990 	private string label;
2991 	private ushort iconId;
2992 	// icon
2993 
2994 	// when it is triggered, the triggered event is fired on the window
2995 	/// The list of handlers when it is triggered.
2996 	void delegate()[] triggered;
2997 }
2998 
2999 /*
3000 	plan:
3001 		keyboard accelerators
3002 
3003 		* menus (and popups and tooltips)
3004 		* status bar
3005 		* toolbars and buttons
3006 
3007 		sortable table view
3008 
3009 		maybe notification area icons
3010 		basic clipboard
3011 
3012 		* radio box
3013 		splitter
3014 		toggle buttons (optionally mutually exclusive, like in Paint)
3015 		label, rich text display, multi line plain text (selectable)
3016 		* fieldset
3017 		* nestable grid layout
3018 		single line text input
3019 		* multi line text input
3020 		slider
3021 		spinner
3022 		list box
3023 		drop down
3024 		combo box
3025 		auto complete box
3026 		* progress bar
3027 
3028 		terminal window/widget (on unix it might even be a pty but really idk)
3029 
3030 		ok button
3031 		cancel button
3032 
3033 		keyboard hotkeys
3034 
3035 		scroll widget
3036 
3037 		event redirections and network transparency
3038 		script integration
3039 */
3040 
3041 
3042 /*
3043 	MENUS
3044 
3045 	auto bar = new MenuBar(window);
3046 	window.menuBar = bar;
3047 
3048 	auto fileMenu = bar.addItem(new Menu("&File"));
3049 	fileMenu.addItem(new MenuItem("&Exit"));
3050 
3051 
3052 	EVENTS
3053 
3054 	For controls, you should usually use "triggered" rather than "click", etc., because
3055 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3056 	This is the case on menus and pushbuttons.
3057 
3058 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3059 */
3060 
3061 
3062 /*
3063 enum LinePreference {
3064 	AlwaysOnOwnLine, // always on its own line
3065 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3066 	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
3067 }
3068 */
3069 
3070 /++
3071 	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.
3072 
3073 	---
3074 	class MyWidget : Widget {
3075 		this(Widget parent) { super(parent); }
3076 
3077 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3078 		mixin Padding!q{4};
3079 
3080 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3081 		mixin Margin!q{8};
3082 
3083 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3084 		// while Top/Bottom/Right remain 8 from the mixin above.
3085 		override int marginLeft() { return 2; }
3086 	}
3087 	---
3088 
3089 
3090 	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]).
3091 
3092 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3093 
3094 	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!).
3095 
3096 	* 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.
3097 +/
3098 mixin template Padding(string code) {
3099 	override int paddingLeft() { return mixin(code);}
3100 	override int paddingRight() { return mixin(code);}
3101 	override int paddingTop() { return mixin(code);}
3102 	override int paddingBottom() { return mixin(code);}
3103 }
3104 
3105 /// ditto
3106 mixin template Margin(string code) {
3107 	override int marginLeft() { return mixin(code);}
3108 	override int marginRight() { return mixin(code);}
3109 	override int marginTop() { return mixin(code);}
3110 	override int marginBottom() { return mixin(code);}
3111 }
3112 
3113 private
3114 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3115 	enum calcingV = relevantMeasure == "height";
3116 
3117 	parent.registerMovement();
3118 
3119 	if(parent.children.length == 0)
3120 		return;
3121 
3122 	auto parentStyle = parent.getComputedStyle();
3123 
3124 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3125 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3126 
3127 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3128 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3129 
3130 	// my own width and height should already be set by the caller of this function...
3131 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3132 		mixin("parentStyle.padding"~firstThingy~"()") -
3133 		mixin("parentStyle.padding"~secondThingy~"()");
3134 
3135 	int stretchinessSum;
3136 	int stretchyChildSum;
3137 	int lastMargin = 0;
3138 
3139 	int shrinkinessSum;
3140 	int shrinkyChildSum;
3141 
3142 	// set initial size
3143 	foreach(child; parent.children) {
3144 
3145 		auto childStyle = child.getComputedStyle();
3146 
3147 		if(cast(StaticPosition) child)
3148 			continue;
3149 		if(child.hidden)
3150 			continue;
3151 
3152 		const iw = child.flexBasisWidth();
3153 		const ih = child.flexBasisHeight();
3154 
3155 		static if(calcingV) {
3156 			child.width = parent.width -
3157 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3158 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3159 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3160 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3161 
3162 			if(child.width < 0)
3163 				child.width = 0;
3164 			if(child.width > childStyle.maxWidth())
3165 				child.width = childStyle.maxWidth();
3166 
3167 			if(iw > 0) {
3168 				auto totalPossible = child.width;
3169 				if(child.width > iw && child.widthStretchiness() == 0)
3170 					child.width = iw;
3171 			}
3172 
3173 			child.height = mymax(childStyle.minHeight(), ih);
3174 		} else {
3175 			// set to take all the space
3176 			child.height = parent.height -
3177 				mixin("childStyle.margin"~firstThingy~"()") -
3178 				mixin("childStyle.margin"~secondThingy~"()") -
3179 				mixin("parentStyle.padding"~firstThingy~"()") -
3180 				mixin("parentStyle.padding"~secondThingy~"()");
3181 
3182 			// then clamp it
3183 			if(child.height < 0)
3184 				child.height = 0;
3185 			if(child.height > childStyle.maxHeight())
3186 				child.height = childStyle.maxHeight();
3187 
3188 			// and if possible, respect the ideal target
3189 			if(ih > 0) {
3190 				auto totalPossible = child.height;
3191 				if(child.height > ih && child.heightStretchiness() == 0)
3192 					child.height = ih;
3193 			}
3194 
3195 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3196 			child.width = mymax(childStyle.minWidth(), iw);
3197 		}
3198 
3199 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3200 
3201 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3202 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3203 		lastMargin = margin;
3204 		spaceRemaining -= thisMargin + margin;
3205 
3206 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3207 		stretchinessSum += s;
3208 		if(s > 0)
3209 			stretchyChildSum++;
3210 
3211 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3212 		shrinkinessSum += s2;
3213 		if(s2 > 0)
3214 			shrinkyChildSum++;
3215 	}
3216 
3217 	if(spaceRemaining < 0 && shrinkyChildSum) {
3218 		// shrink to get into the space if it is possible
3219 		auto toRemove = -spaceRemaining;
3220 		auto removalPerItem = toRemove / shrinkinessSum;
3221 		auto remainder = toRemove % shrinkinessSum;
3222 
3223 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3224 
3225 		foreach(child; parent.children) {
3226 			auto childStyle = child.getComputedStyle();
3227 			if(cast(StaticPosition) child)
3228 				continue;
3229 			if(child.hidden)
3230 				continue;
3231 			static if(calcingV) {
3232 				auto minimum = childStyle.minHeight();
3233 				auto stretch = childStyle.heightShrinkiness();
3234 			} else {
3235 				auto minimum = childStyle.minWidth();
3236 				auto stretch = childStyle.widthShrinkiness();
3237 			}
3238 
3239 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3240 				continue;
3241 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3242 
3243 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3244 
3245 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3246 		}
3247 	}
3248 
3249 	// stretch to fill space
3250 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3251 		auto spacePerChild = spaceRemaining / stretchinessSum;
3252 		bool spreadEvenly;
3253 		bool giveToBiggest;
3254 		if(spacePerChild <= 0) {
3255 			spacePerChild = spaceRemaining / stretchyChildSum;
3256 			spreadEvenly = true;
3257 		}
3258 		if(spacePerChild <= 0) {
3259 			giveToBiggest = true;
3260 		}
3261 		int previousSpaceRemaining = spaceRemaining;
3262 		stretchinessSum = 0;
3263 		Widget mostStretchy;
3264 		int mostStretchyS;
3265 		foreach(child; parent.children) {
3266 			auto childStyle = child.getComputedStyle();
3267 			if(cast(StaticPosition) child)
3268 				continue;
3269 			if(child.hidden)
3270 				continue;
3271 			static if(calcingV) {
3272 				auto maximum = childStyle.maxHeight();
3273 			} else {
3274 				auto maximum = childStyle.maxWidth();
3275 			}
3276 
3277 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3278 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3279 				mixin("child._" ~ relevantMeasure) -= adj;
3280 				spaceRemaining += adj;
3281 				continue;
3282 			}
3283 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3284 			if(s <= 0)
3285 				continue;
3286 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3287 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3288 			spaceRemaining -= spaceAdjustment;
3289 			if(mixin("child." ~ relevantMeasure) > maximum) {
3290 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3291 				mixin("child._" ~ relevantMeasure) -= diff;
3292 				spaceRemaining += diff;
3293 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3294 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3295 				if(mostStretchy is null || s >= mostStretchyS) {
3296 					mostStretchy = child;
3297 					mostStretchyS = s;
3298 				}
3299 			}
3300 		}
3301 
3302 		if(giveToBiggest && mostStretchy !is null) {
3303 			auto child = mostStretchy;
3304 			auto childStyle = child.getComputedStyle();
3305 			int spaceAdjustment = spaceRemaining;
3306 
3307 			static if(calcingV)
3308 				auto maximum = childStyle.maxHeight();
3309 			else
3310 				auto maximum = childStyle.maxWidth();
3311 
3312 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3313 			spaceRemaining -= spaceAdjustment;
3314 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3315 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3316 				mixin("child._" ~ relevantMeasure) -= diff;
3317 				spaceRemaining += diff;
3318 			}
3319 		}
3320 
3321 		if(spaceRemaining == previousSpaceRemaining) {
3322 			if(mostStretchy !is null) {
3323 				static if(calcingV)
3324 					auto maximum = mostStretchy.maxHeight();
3325 				else
3326 					auto maximum = mostStretchy.maxWidth();
3327 
3328 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3329 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3330 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3331 			}
3332 			break; // apparently nothing more we can do
3333 		}
3334 	}
3335 
3336 	foreach(child; parent.children) {
3337 		auto childStyle = child.getComputedStyle();
3338 		if(cast(StaticPosition) child)
3339 			continue;
3340 		if(child.hidden)
3341 			continue;
3342 
3343 		static if(calcingV)
3344 			auto maximum = childStyle.maxHeight();
3345 		else
3346 			auto maximum = childStyle.maxWidth();
3347 		if(mixin("child._" ~ relevantMeasure) > maximum)
3348 			mixin("child._" ~ relevantMeasure) = maximum;
3349 	}
3350 
3351 	// position
3352 	lastMargin = 0;
3353 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3354 	foreach(child; parent.children) {
3355 		auto childStyle = child.getComputedStyle();
3356 		if(cast(StaticPosition) child) {
3357 			child.recomputeChildLayout();
3358 			continue;
3359 		}
3360 		if(child.hidden)
3361 			continue;
3362 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3363 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3364 		currentPos += thisMargin;
3365 		static if(calcingV) {
3366 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3367 			child.y = currentPos;
3368 		} else {
3369 			child.x = currentPos;
3370 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3371 
3372 		}
3373 		currentPos += mixin("child." ~ relevantMeasure);
3374 		currentPos += margin;
3375 		lastMargin = margin;
3376 
3377 		child.recomputeChildLayout();
3378 	}
3379 }
3380 
3381 int mymax(int a, int b) { return a > b ? a : b; }
3382 int mymax(int a, int b, int c) {
3383 	auto d = mymax(a, b);
3384 	return c > d ? c : d;
3385 }
3386 
3387 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3388 // and here, it must be integrable with the layout, the event system, and not be painted over.
3389 version(win32_widgets) {
3390 
3391 	// this function just does stuff that a parent window needs for redirection
3392 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3393 		this_.hookedWndProc(msg, wParam, lParam);
3394 
3395 		switch(msg) {
3396 
3397 			case WM_VSCROLL, WM_HSCROLL:
3398 				auto pos = HIWORD(wParam);
3399 				auto m = LOWORD(wParam);
3400 
3401 				auto scrollbarHwnd = cast(HWND) lParam;
3402 
3403 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3404 
3405 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3406 
3407 					switch(m) {
3408 						/+
3409 						// I don't think those messages are ever actually sent normally by the widget itself,
3410 						// they are more used for the keyboard interface. methinks.
3411 						case SB_BOTTOM:
3412 							// writeln("end");
3413 							auto event = new Event("scrolltoend", *widgetp);
3414 							event.dispatch();
3415 							//if(!event.defaultPrevented)
3416 						break;
3417 						case SB_TOP:
3418 							// writeln("top");
3419 							auto event = new Event("scrolltobeginning", *widgetp);
3420 							event.dispatch();
3421 						break;
3422 						case SB_ENDSCROLL:
3423 							// idk
3424 						break;
3425 						+/
3426 						case SB_LINEDOWN:
3427 							(*widgetp).emitCommand!"scrolltonextline"();
3428 						return 0;
3429 						case SB_LINEUP:
3430 							(*widgetp).emitCommand!"scrolltopreviousline"();
3431 						return 0;
3432 						case SB_PAGEDOWN:
3433 							(*widgetp).emitCommand!"scrolltonextpage"();
3434 						return 0;
3435 						case SB_PAGEUP:
3436 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3437 						return 0;
3438 						case SB_THUMBPOSITION:
3439 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3440 							ev.dispatch();
3441 						return 0;
3442 						case SB_THUMBTRACK:
3443 							// eh kinda lying but i like the real time update display
3444 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3445 							ev.dispatch();
3446 
3447 							// the event loop doesn't seem to carry on with a requested redraw..
3448 							// so we request it to get our dirty bit set...
3449 							// then we need to immediately actually redraw it too for instant feedback to user
3450 							SimpleWindow.processAllCustomEvents();
3451 							SimpleWindow.processAllCustomEvents();
3452 							//if(this_.parentWindow)
3453 								//this_.parentWindow.actualRedraw();
3454 
3455 							// and this ensures the WM_PAINT message is sent fairly quickly
3456 							// still seems to lag a little in large windows but meh it basically works.
3457 							if(this_.parentWindow) {
3458 								// FIXME: if painting is slow, this does still lag
3459 								// we probably will want to expose some user hook to ScrollWindowEx
3460 								// or something.
3461 								UpdateWindow(this_.parentWindow.hwnd);
3462 							}
3463 						return 0;
3464 						default:
3465 					}
3466 				}
3467 			break;
3468 
3469 			case WM_CONTEXTMENU:
3470 				auto hwndFrom = cast(HWND) wParam;
3471 
3472 				auto xPos = cast(short) LOWORD(lParam);
3473 				auto yPos = cast(short) HIWORD(lParam);
3474 
3475 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3476 					POINT p;
3477 					p.x = xPos;
3478 					p.y = yPos;
3479 					ScreenToClient(hwnd, &p);
3480 					auto clientX = cast(ushort) p.x;
3481 					auto clientY = cast(ushort) p.y;
3482 
3483 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
3484 
3485 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
3486 						return 0;
3487 					}
3488 				}
3489 			break;
3490 
3491 			case WM_DRAWITEM:
3492 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
3493 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
3494 					return (*widgetp).handleWmDrawItem(dis);
3495 				}
3496 			break;
3497 
3498 			case WM_NOTIFY:
3499 				auto hdr = cast(NMHDR*) lParam;
3500 				auto hwndFrom = hdr.hwndFrom;
3501 				auto code = hdr.code;
3502 
3503 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3504 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
3505 				}
3506 			break;
3507 			case WM_COMMAND:
3508 				auto handle = cast(HWND) lParam;
3509 				auto cmd = HIWORD(wParam);
3510 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
3511 
3512 			default:
3513 				// pass it on
3514 		}
3515 		return 0;
3516 	}
3517 
3518 
3519 
3520 	extern(Windows)
3521 	private
3522 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
3523 	// but can i merge them?!
3524 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3525 		// try { writeln(iMessage); } catch(Exception e) {};
3526 
3527 		if(auto te = hWnd in Widget.nativeMapping) {
3528 			try {
3529 
3530 				te.hookedWndProc(iMessage, wParam, lParam);
3531 
3532 				int mustReturn;
3533 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
3534 				if(mustReturn)
3535 					return ret;
3536 
3537 				if(iMessage == WM_SETFOCUS) {
3538 					auto lol = *te;
3539 					while(lol !is null && lol.implicitlyCreated)
3540 						lol = lol.parent;
3541 					lol.focus();
3542 					//(*te).parentWindow.focusedWidget = lol;
3543 				}
3544 
3545 
3546 				if(iMessage == WM_CTLCOLOREDIT) {
3547 
3548 				}
3549 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
3550 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
3551 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
3552 						//GetStockObject(NULL_BRUSH);
3553 				}
3554 
3555 				auto pos = getChildPositionRelativeToParentOrigin(*te);
3556 				lastDefaultPrevented = false;
3557 				// try { writeln(typeid(*te)); } catch(Exception e) {}
3558 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
3559 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
3560 				else {
3561 					// it was something we recognized, should only call the window procedure if the default was not prevented
3562 				}
3563 			} catch(Exception e) {
3564 				assert(0, e.toString());
3565 			}
3566 			return 0;
3567 		}
3568 		assert(0, "shouldn't be receiving messages for this window....");
3569 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
3570 	}
3571 
3572 	extern(Windows)
3573 	private
3574 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
3575 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3576 		if(iMessage == WM_ERASEBKGND) {
3577 			auto dc = GetDC(hWnd);
3578 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
3579 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
3580 			RECT r;
3581 			GetWindowRect(hWnd, &r);
3582 			// since the pen is null, to fill the whole space, we need the +1 on both.
3583 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
3584 			SelectObject(dc, p);
3585 			SelectObject(dc, b);
3586 			ReleaseDC(hWnd, dc);
3587 			InvalidateRect(hWnd, null, false); // redraw the border
3588 			return 1;
3589 		}
3590 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
3591 	}
3592 
3593 	/++
3594 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
3595 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
3596 		of minigui's expectations.
3597 
3598 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
3599 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
3600 
3601 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
3602 
3603 		To check if you can use this, use `static if(UsingWin32Widgets)`.
3604 	+/
3605 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
3606 		assert(p.parentWindow !is null);
3607 		assert(p.parentWindow.win.impl.hwnd !is null);
3608 
3609 		auto bsgroupbox = style == BS_GROUPBOX;
3610 
3611 		HWND phwnd;
3612 
3613 		auto wtf = p.parent;
3614 		while(wtf) {
3615 			if(wtf.hwnd !is null) {
3616 				phwnd = wtf.hwnd;
3617 				break;
3618 			}
3619 			wtf = wtf.parent;
3620 		}
3621 
3622 		if(phwnd is null)
3623 			phwnd = p.parentWindow.win.impl.hwnd;
3624 
3625 		assert(phwnd !is null);
3626 
3627 		WCharzBuffer wt = WCharzBuffer(windowText);
3628 
3629 		style |= WS_VISIBLE | WS_CHILD;
3630 		//if(className != WC_TABCONTROL)
3631 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
3632 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
3633 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
3634 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
3635 
3636 		assert(p.hwnd !is null);
3637 
3638 
3639 		static HFONT font;
3640 		if(font is null) {
3641 			NONCLIENTMETRICS params;
3642 			params.cbSize = params.sizeof;
3643 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
3644 				font = CreateFontIndirect(&params.lfMessageFont);
3645 			}
3646 		}
3647 
3648 		if(font)
3649 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
3650 
3651 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
3652 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
3653 		Widget.nativeMapping[p.hwnd] = p;
3654 
3655 		if(bsgroupbox)
3656 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
3657 		else
3658 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3659 
3660 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
3661 
3662 		p.registerMovement();
3663 	}
3664 }
3665 
3666 version(win32_widgets)
3667 private
3668 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
3669 	if(hwnd is null || hwnd in Widget.nativeMapping)
3670 		return true;
3671 	auto parent = cast(Widget) cast(void*) lparam;
3672 	Widget p = new Widget(null);
3673 	p._parent = parent;
3674 	p.parentWindow = parent.parentWindow;
3675 	p.hwnd = hwnd;
3676 	p.implicitlyCreated = true;
3677 	Widget.nativeMapping[p.hwnd] = p;
3678 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3679 	return true;
3680 }
3681 
3682 /++
3683 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
3684 +/
3685 struct WidgetPainter {
3686 	this(ScreenPainter screenPainter, Widget drawingUpon) {
3687 		this.drawingUpon = drawingUpon;
3688 		this.screenPainter = screenPainter;
3689 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3690 			this.screenPainter.setFont(font);
3691 	}
3692 
3693 	/++
3694 		EXPERIMENTAL. subject to change.
3695 
3696 		When you draw a cursor, you can draw this to notify your window of where it is,
3697 		for IME systems to use.
3698 	+/
3699 	void notifyCursorPosition(int x, int y, int width, int height) {
3700 		if(auto a = drawingUpon.parentWindow)
3701 		if(auto w = a.inputProxy) {
3702 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
3703 		}
3704 	}
3705 
3706 
3707 	///
3708 	ScreenPainter screenPainter;
3709 	/// Forward to the screen painter for other methods
3710 	alias screenPainter this;
3711 
3712 	private Widget drawingUpon;
3713 
3714 	/++
3715 		This is the list of rectangles that actually need to be redrawn.
3716 
3717 		Not actually implemented yet.
3718 	+/
3719 	Rectangle[] invalidatedRectangles;
3720 
3721 	private static BaseVisualTheme _visualTheme;
3722 
3723 	/++
3724 		Functions to access the visual theme and helpers to easily use it.
3725 
3726 		These are aware of the current widget's computed style out of the theme.
3727 	+/
3728 	static @property BaseVisualTheme visualTheme() {
3729 		if(_visualTheme is null)
3730 			_visualTheme = new DefaultVisualTheme();
3731 		return _visualTheme;
3732 	}
3733 
3734 	/// ditto
3735 	static @property void visualTheme(BaseVisualTheme theme) {
3736 		_visualTheme = theme;
3737 
3738 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
3739 	}
3740 
3741 	/// ditto
3742 	Color themeForeground() {
3743 		return drawingUpon.getComputedStyle().foregroundColor();
3744 	}
3745 
3746 	/// ditto
3747 	Color themeBackground() {
3748 		return drawingUpon.getComputedStyle().background.color;
3749 	}
3750 
3751 	int isDarkTheme() {
3752 		return 0; // unspecified, yes, no as enum. FIXME
3753 	}
3754 
3755 	/++
3756 		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.
3757 
3758 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
3759 
3760 		If you change teh clip rectangle, you should change it back before you return.
3761 
3762 
3763 		The sequence it uses is:
3764 			background
3765 			content (delegated to you)
3766 			border
3767 			focused outline
3768 			selected overlay
3769 
3770 		Example code:
3771 
3772 		---
3773 		void paint(WidgetPainter painter) {
3774 			painter.drawThemed((bounds) {
3775 				return bounds; // if the selection overlay should be contained, you can return it here.
3776 			});
3777 		}
3778 		---
3779 	+/
3780 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
3781 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
3782 			return drawBody(bounds);
3783 		});
3784 	}
3785 	// this overload is actually mroe for setting the delegate to a virtual function
3786 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
3787 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
3788 
3789 		auto cs = drawingUpon.getComputedStyle();
3790 
3791 		auto bg = cs.background.color;
3792 
3793 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
3794 
3795 		rect.left += borderWidth;
3796 		rect.right -= borderWidth;
3797 		rect.top += borderWidth;
3798 		rect.bottom -= borderWidth;
3799 
3800 		auto insideBorderRect = rect;
3801 
3802 		rect.left += cs.paddingLeft;
3803 		rect.right -= cs.paddingRight;
3804 		rect.top += cs.paddingTop;
3805 		rect.bottom -= cs.paddingBottom;
3806 
3807 		this.outlineColor = this.themeForeground;
3808 		this.fillColor = bg;
3809 
3810 		auto widgetFont = cs.fontCached;
3811 		if(widgetFont !is null)
3812 			this.setFont(widgetFont);
3813 
3814 		rect = drawBody(this, rect);
3815 
3816 		if(widgetFont !is null) {
3817 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3818 				this.setFont(vtFont);
3819 			else
3820 				this.setFont(null);
3821 		}
3822 
3823 		if(auto os = cs.outlineStyle()) {
3824 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
3825 			this.fillColor = Color.transparent;
3826 			this.drawRectangle(insideBorderRect);
3827 		}
3828 	}
3829 
3830 	/++
3831 		First, draw the background.
3832 		Then draw your content.
3833 		Next, draw the border.
3834 		And the focused indicator.
3835 		And the is-selected box.
3836 
3837 		If it is focused i can draw the outline too...
3838 
3839 		If selected i can even do the xor action but that's at the end.
3840 	+/
3841 	void drawThemeBackground() {
3842 
3843 	}
3844 
3845 	void drawThemeBorder() {
3846 
3847 	}
3848 
3849 	// all this stuff is a dangerous experiment....
3850 	static class ScriptableVersion {
3851 		ScreenPainterImplementation* p;
3852 		int originX, originY;
3853 
3854 		@scriptable:
3855 		void drawRectangle(int x, int y, int width, int height) {
3856 			p.drawRectangle(x + originX, y + originY, width, height);
3857 		}
3858 		void drawLine(int x1, int y1, int x2, int y2) {
3859 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
3860 		}
3861 		void drawText(int x, int y, string text) {
3862 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
3863 		}
3864 		void setOutlineColor(int r, int g, int b) {
3865 			p.pen = Pen(Color(r,g,b), 1);
3866 		}
3867 		void setFillColor(int r, int g, int b) {
3868 			p.fillColor = Color(r,g,b);
3869 		}
3870 	}
3871 
3872 	ScriptableVersion toArsdJsvar() {
3873 		auto sv = new ScriptableVersion;
3874 		sv.p = this.screenPainter.impl;
3875 		sv.originX = this.screenPainter.originX;
3876 		sv.originY = this.screenPainter.originY;
3877 		return sv;
3878 	}
3879 
3880 	static WidgetPainter fromJsVar(T)(T t) {
3881 		return WidgetPainter.init;
3882 	}
3883 	// done..........
3884 }
3885 
3886 
3887 struct Style {
3888 	static struct helper(string m, T) {
3889 		enum method = m;
3890 		T v;
3891 
3892 		mixin template MethodOverride(typeof(this) v) {
3893 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
3894 		}
3895 	}
3896 
3897 	static auto opDispatch(string method, T)(T value) {
3898 		return helper!(method, T)(value);
3899 	}
3900 }
3901 
3902 /++
3903 	Implementation detail of the [ControlledBy] UDA.
3904 
3905 	History:
3906 		Added Oct 28, 2020
3907 +/
3908 struct ControlledBy_(T, Args...) {
3909 	Args args;
3910 
3911 	static if(Args.length)
3912 	this(Args args) {
3913 		this.args = args;
3914 	}
3915 
3916 	private T construct(Widget parent) {
3917 		return new T(args, parent);
3918 	}
3919 }
3920 
3921 /++
3922 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
3923 
3924 	History:
3925 		Added Oct 28, 2020
3926 +/
3927 auto ControlledBy(T, Args...)(Args args) {
3928 	return ControlledBy_!(T, Args)(args);
3929 }
3930 
3931 struct ContainerMeta {
3932 	string name;
3933 	ContainerMeta[] children;
3934 	Widget function(Widget parent) factory;
3935 
3936 	Widget instantiate(Widget parent) {
3937 		auto n = factory(parent);
3938 		n.name = name;
3939 		foreach(child; children)
3940 			child.instantiate(n);
3941 		return n;
3942 	}
3943 }
3944 
3945 /++
3946 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
3947 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
3948 
3949 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
3950 	structures. It works fine on structs declared inside functions though.
3951 
3952 	See: https://issues.dlang.org/show_bug.cgi?id=21984
3953 +/
3954 template Container(CArgs...) {
3955 	static if(CArgs.length && is(CArgs[0] : Widget)) {
3956 		private alias Super = CArgs[0];
3957 		private alias CArgs2 = CArgs[1 .. $];
3958 	} else {
3959 		private alias Super = Layout;
3960 		private alias CArgs2 = CArgs;
3961 	}
3962 
3963 	class Container : Super {
3964 		this(Widget parent) { super(parent); }
3965 
3966 		// just to partially support old gdc versions
3967 		version(GNU) {
3968 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
3969 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
3970 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
3971 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
3972 		} else mixin(q{
3973 			static foreach(Arg; CArgs2) {
3974 				mixin Arg.MethodOverride!(Arg);
3975 			}
3976 		});
3977 
3978 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
3979 			return ContainerMeta(
3980 				name,
3981 				children.dup,
3982 				function (Widget parent) { return new typeof(this)(parent); }
3983 			);
3984 		}
3985 
3986 		static ContainerMeta opCall(ContainerMeta[] children...) {
3987 			return opCall(null, children);
3988 		}
3989 	}
3990 }
3991 
3992 /++
3993 	The data controller widget is created by reflecting over the given
3994 	data type. You can use [ControlledBy] as a UDA on a struct or
3995 	just let it create things automatically.
3996 
3997 	Unlike [dialog], this uses real-time updating of the data and
3998 	you add it to another window yourself.
3999 
4000 	---
4001 		struct Test {
4002 			int x;
4003 			int y;
4004 		}
4005 
4006 		auto window = new Window();
4007 		auto dcw = new DataControllerWidget!Test(new Test, window);
4008 	---
4009 
4010 	The way it works is any public members are given a widget based
4011 	on their data type, and public methods trigger an action button
4012 	if no relevant parameters or a dialog action if it does have
4013 	parameters, similar to the [menu] facility.
4014 
4015 	If you change data programmatically, without going through the
4016 	DataControllerWidget methods, you will have to tell it something
4017 	has changed and it needs to redraw. This is done with the `invalidate`
4018 	method.
4019 
4020 	History:
4021 		Added Oct 28, 2020
4022 +/
4023 /// Group: generating_from_code
4024 class DataControllerWidget(T) : WidgetContainer {
4025 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4026 		private alias Tref = T;
4027 	else
4028 		private alias Tref = T*;
4029 
4030 	Tref datum;
4031 
4032 	/++
4033 		See_also: [addDataControllerWidget]
4034 	+/
4035 	this(Tref datum, Widget parent) {
4036 		this.datum = datum;
4037 
4038 		Widget cp = this;
4039 
4040 		super(parent);
4041 
4042 		foreach(attr; __traits(getAttributes, T))
4043 			static if(is(typeof(attr) == ContainerMeta)) {
4044 				cp = attr.instantiate(this);
4045 			}
4046 
4047 		auto def = this.getByName("default");
4048 		if(def !is null)
4049 			cp = def;
4050 
4051 		Widget helper(string name) {
4052 			auto maybe = this.getByName(name);
4053 			if(maybe is null)
4054 				return cp;
4055 			return maybe;
4056 
4057 		}
4058 
4059 		foreach(member; __traits(allMembers, T))
4060 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4061 		static if(is(typeof(__traits(getMember, this.datum, member))))
4062 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4063 			void delegate() update;
4064 
4065 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4066 
4067 			if(update)
4068 				updaters ~= update;
4069 
4070 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4071 				w.addEventListener("triggered", delegate() {
4072 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4073 					notifyDataUpdated();
4074 				});
4075 			} else static if(is(typeof(w.isChecked) == bool)) {
4076 				w.addEventListener(EventType.change, (Event ev) {
4077 					__traits(getMember, this.datum, member) = w.isChecked;
4078 				});
4079 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4080 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4081 			} else static if(is(typeof(w.value) == int)) {
4082 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4083 			} else static if(is(typeof(w) == DropDownSelection)) {
4084 				// special case for this to kinda support enums and such. coudl be better though
4085 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4086 			} else {
4087 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4088 			}
4089 		}
4090 	}
4091 
4092 	/++
4093 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4094 
4095 		History:
4096 			Added May 28, 2021
4097 	+/
4098 	void notifyDataUpdated() {
4099 		foreach(updater; updaters)
4100 			updater();
4101 
4102 		this.emit!(ChangeEvent!void)(delegate{});
4103 	}
4104 
4105 	private Widget[string] memberWidgets;
4106 	private void delegate()[] updaters;
4107 
4108 	mixin Emits!(ChangeEvent!void);
4109 }
4110 
4111 private int saturatedSum(int[] values...) {
4112 	int sum;
4113 	foreach(value; values) {
4114 		if(value == int.max)
4115 			return int.max;
4116 		sum += value;
4117 	}
4118 	return sum;
4119 }
4120 
4121 void genericSetValue(T, W)(T* where, W what) {
4122 	import std.conv;
4123 	*where = to!T(what);
4124 	//*where = cast(T) stringToLong(what);
4125 }
4126 
4127 /++
4128 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4129 
4130 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4131 
4132 	Note that this creates the widget but does not attach any event handlers to it.
4133 +/
4134 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4135 
4136 	string displayName = __traits(identifier, tt).beautify;
4137 
4138 	static if(controlledByCount!tt == 1) {
4139 		foreach(i, attr; __traits(getAttributes, tt)) {
4140 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4141 				auto w = attr.construct(parent);
4142 				static if(__traits(compiles, w.setPosition(*valptr)))
4143 					update = () { w.setPosition(*valptr); };
4144 				else static if(__traits(compiles, w.setValue(*valptr)))
4145 					update = () { w.setValue(*valptr); };
4146 
4147 				if(update)
4148 					update();
4149 				return w;
4150 			}
4151 		}
4152 	} else static if(controlledByCount!tt == 0) {
4153 		static if(is(typeof(tt) == enum)) {
4154 			// FIXME: update
4155 			auto dds = new DropDownSelection(parent);
4156 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4157 				dds.addOption(option);
4158 				if(__traits(getMember, typeof(tt), option) == *valptr)
4159 					dds.setSelection(cast(int) idx);
4160 			}
4161 			return dds;
4162 		} else static if(is(typeof(tt) == bool)) {
4163 			auto box = new Checkbox(displayName, parent);
4164 			update = () { box.isChecked = *valptr; };
4165 			update();
4166 			return box;
4167 		} else static if(is(typeof(tt) : const long)) {
4168 			auto le = new LabeledLineEdit(displayName, parent);
4169 			update = () { le.content = toInternal!string(*valptr); };
4170 			update();
4171 			return le;
4172 		} else static if(is(typeof(tt) : const double)) {
4173 			auto le = new LabeledLineEdit(displayName, parent);
4174 			import std.conv;
4175 			update = () { le.content = to!string(*valptr); };
4176 			update();
4177 			return le;
4178 		} else static if(is(typeof(tt) : const string)) {
4179 			auto le = new LabeledLineEdit(displayName, parent);
4180 			update = () { le.content = *valptr; };
4181 			update();
4182 			return le;
4183 		} else static if(is(typeof(tt) == function)) {
4184 			auto w = new Button(displayName, parent);
4185 			return w;
4186 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4187 			return parent.addDataControllerWidget(tt);
4188 		} else static assert(0, typeof(tt).stringof);
4189 	} else static assert(0, "multiple controllers not yet supported");
4190 }
4191 
4192 private template controlledByCount(alias tt) {
4193 	static int helper() {
4194 		int count;
4195 		foreach(i, attr; __traits(getAttributes, tt))
4196 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4197 				count++;
4198 		return count;
4199 	}
4200 
4201 	enum controlledByCount = helper;
4202 }
4203 
4204 /++
4205 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4206 
4207 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4208 
4209 	History:
4210 		The `redrawOnChange` parameter was added on May 28, 2021.
4211 +/
4212 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4213 	auto dcw = new DataControllerWidget!T(t, parent);
4214 	initializeDataControllerWidget(dcw, redrawOnChange);
4215 	return dcw;
4216 }
4217 
4218 /// ditto
4219 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4220 	auto dcw = new DataControllerWidget!T(t, parent);
4221 	initializeDataControllerWidget(dcw, redrawOnChange);
4222 	return dcw;
4223 }
4224 
4225 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4226 	if(redrawOnChange !is null)
4227 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4228 }
4229 
4230 /++
4231 	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.
4232 
4233 	History:
4234 		Finalized on June 3, 2021 for the dub v10.0 release
4235 +/
4236 struct StyleInformation {
4237 	private Widget w;
4238 	private BaseVisualTheme visualTheme;
4239 
4240 	private this(Widget w) {
4241 		this.w = w;
4242 		this.visualTheme = WidgetPainter.visualTheme;
4243 	}
4244 
4245 	/++
4246 		Forwards to [Widget.Style]
4247 
4248 		Bugs:
4249 			It is supposed to fall back to the [VisualTheme] if
4250 			the style doesn't override the default, but that is
4251 			not generally implemented. Many of them may end up
4252 			being explicit overloads instead of the generic
4253 			opDispatch fallback, like [font] is now.
4254 	+/
4255 	public @property opDispatch(string name)() {
4256 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4257 		w.useStyleProperties((scope Widget.Style props) {
4258 		//visualTheme.useStyleProperties(w, (props) {
4259 			prop = __traits(getMember, props, name);
4260 		});
4261 		return prop;
4262 	}
4263 
4264 	/++
4265 		Returns the cached font object associated with the widget,
4266 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4267 
4268 		History:
4269 			Prior to March 21, 2022 (dub v10.7), `font` went through
4270 			[opDispatch], which did not use the cache. You can now call it
4271 			repeatedly without guilt.
4272 	+/
4273 	public @property OperatingSystemFont font() {
4274 		OperatingSystemFont prop;
4275 		w.useStyleProperties((scope Widget.Style props) {
4276 			prop = props.fontCached;
4277 		});
4278 		if(prop is null) {
4279 			prop = visualTheme.defaultFontCached(w.currentDpi);
4280 		}
4281 		return prop;
4282 	}
4283 
4284 	@property {
4285 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4286 		/** */ int paddingLeft() { return w.paddingLeft(); }
4287 		/** */ int paddingRight() { return w.paddingRight(); }
4288 		/** */ int paddingTop() { return w.paddingTop(); }
4289 		/** */ int paddingBottom() { return w.paddingBottom(); }
4290 
4291 		/** */ int marginLeft() { return w.marginLeft(); }
4292 		/** */ int marginRight() { return w.marginRight(); }
4293 		/** */ int marginTop() { return w.marginTop(); }
4294 		/** */ int marginBottom() { return w.marginBottom(); }
4295 
4296 		/** */ int maxHeight() { return w.maxHeight(); }
4297 		/** */ int minHeight() { return w.minHeight(); }
4298 
4299 		/** */ int maxWidth() { return w.maxWidth(); }
4300 		/** */ int minWidth() { return w.minWidth(); }
4301 
4302 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4303 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4304 
4305 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4306 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4307 
4308 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4309 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4310 
4311 		// Global helpers some of these are unstable.
4312 		static:
4313 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4314 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4315 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4316 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4317 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4318 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4319 
4320 		/** */ Color activeTabColor() { return lightAccentColor; }
4321 		/** */ Color buttonColor() { return windowBackgroundColor; }
4322 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4323 		/** */ Color hoveringColor() { return lightAccentColor; }
4324 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4325 			auto c = WidgetPainter.visualTheme.selectionColor();
4326 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4327 		}
4328 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4329 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4330 	}
4331 
4332 
4333 
4334 	/+
4335 
4336 	private static auto extractStyleProperty(string name)(Widget w) {
4337 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4338 		w.useStyleProperties((props) {
4339 			prop = __traits(getMember, props, name);
4340 		});
4341 		return prop;
4342 	}
4343 
4344 	// FIXME: clear this upon a X server disconnect
4345 	private static OperatingSystemFont[string] fontCache;
4346 
4347 	T getProperty(T)(string name, lazy T default_) {
4348 		if(visualTheme !is null) {
4349 			auto str = visualTheme.getPropertyString(w, name);
4350 			if(str is null)
4351 				return default_;
4352 			static if(is(T == Color))
4353 				return Color.fromString(str);
4354 			else static if(is(T == Measurement))
4355 				return Measurement(cast(int) toInternal!int(str));
4356 			else static if(is(T == WidgetBackground))
4357 				return WidgetBackground.fromString(str);
4358 			else static if(is(T == OperatingSystemFont)) {
4359 				if(auto f = str in fontCache)
4360 					return *f;
4361 				else
4362 					return fontCache[str] = new OperatingSystemFont(str);
4363 			} else static if(is(T == FrameStyle)) {
4364 				switch(str) {
4365 					default:
4366 						return FrameStyle.none;
4367 					foreach(style; __traits(allMembers, FrameStyle))
4368 					case style:
4369 						return __traits(getMember, FrameStyle, style);
4370 				}
4371 			} else static assert(0);
4372 		} else
4373 			return default_;
4374 	}
4375 
4376 	static struct Measurement {
4377 		int value;
4378 		alias value this;
4379 	}
4380 
4381 	@property:
4382 
4383 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4384 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4385 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4386 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4387 
4388 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4389 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4390 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4391 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4392 
4393 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4394 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4395 
4396 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4397 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4398 
4399 
4400 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4401 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4402 
4403 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4404 
4405 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4406 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4407 
4408 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4409 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4410 
4411 
4412 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4413 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4414 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4415 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4416 
4417 	Color activeTabColor() { return lightAccentColor; }
4418 	Color buttonColor() { return windowBackgroundColor; }
4419 	Color depressedButtonColor() { return darkAccentColor; }
4420 	Color hoveringColor() { return Color(228, 228, 228); }
4421 	Color activeListXorColor() {
4422 		auto c = WidgetPainter.visualTheme.selectionColor();
4423 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4424 	}
4425 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4426 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4427 	+/
4428 }
4429 
4430 
4431 
4432 // pragma(msg, __traits(classInstanceSize, Widget));
4433 
4434 /*private*/ template EventString(E) {
4435 	static if(is(typeof(E.EventString)))
4436 		enum EventString = E.EventString;
4437 	else
4438 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4439 }
4440 
4441 /*private*/ template EventStringIdentifier(E) {
4442 	string helper() {
4443 		auto es = EventString!E;
4444 		char[] id = new char[](es.length * 2);
4445 		size_t idx;
4446 		foreach(char ch; es) {
4447 			id[idx++] = cast(char)('a' + (ch >> 4));
4448 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4449 		}
4450 		return cast(string) id;
4451 	}
4452 
4453 	enum EventStringIdentifier = helper();
4454 }
4455 
4456 
4457 template classStaticallyEmits(This, EventType) {
4458 	static if(is(This Base == super))
4459 		static if(is(Base : Widget))
4460 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4461 		else
4462 			enum baseEmits = false;
4463 	else
4464 		enum baseEmits = false;
4465 
4466 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
4467 
4468 	enum classStaticallyEmits = thisEmits || baseEmits;
4469 }
4470 
4471 /++
4472 	A helper to make widgets out of other native windows.
4473 
4474 	History:
4475 		Factored out of OpenGlWidget on November 5, 2021
4476 +/
4477 class NestedChildWindowWidget : Widget {
4478 	SimpleWindow win;
4479 
4480 	/++
4481 		Used on X to send focus to the appropriate child window when requested by the window manager.
4482 
4483 		Normally returns its own nested window. Can also return another child or null to revert to the parent
4484 		if you override it in a child class.
4485 
4486 		History:
4487 			Added April 2, 2022 (dub v10.8)
4488 	+/
4489 	SimpleWindow focusableWindow() {
4490 		return win;
4491 	}
4492 
4493 	///
4494 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4495 	this(SimpleWindow win, Widget parent) {
4496 		this.parentWindow = parent.parentWindow;
4497 		this.win = win;
4498 
4499 		super(parent);
4500 		windowsetup(win);
4501 	}
4502 
4503 	static protected SimpleWindow getParentWindow(Widget parent) {
4504 		assert(parent !is null);
4505 		SimpleWindow pwin = parent.parentWindow.win;
4506 
4507 		version(win32_widgets) {
4508 			HWND phwnd;
4509 			auto wtf = parent;
4510 			while(wtf) {
4511 				if(wtf.hwnd) {
4512 					phwnd = wtf.hwnd;
4513 					break;
4514 				}
4515 				wtf = wtf.parent;
4516 			}
4517 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
4518 			if(phwnd)
4519 				pwin = new SimpleWindow(phwnd);
4520 		}
4521 
4522 		return pwin;
4523 	}
4524 
4525 	/++
4526 		Called upon the nested window being destroyed.
4527 		Remember the window has already been destroyed at
4528 		this point, so don't use the native handle for anything.
4529 
4530 		History:
4531 			Added April 3, 2022 (dub v10.8)
4532 	+/
4533 	protected void dispose() {
4534 
4535 	}
4536 
4537 	protected void windowsetup(SimpleWindow w) {
4538 		/*
4539 		win.onFocusChange = (bool getting) {
4540 			if(getting)
4541 				this.focus();
4542 		};
4543 		*/
4544 
4545 		/+
4546 		win.onFocusChange = (bool getting) {
4547 			if(getting) {
4548 				this.parentWindow.focusedWidget = this;
4549 				this.emit!FocusEvent();
4550 				this.emit!FocusInEvent();
4551 			} else {
4552 				this.emit!BlurEvent();
4553 				this.emit!FocusOutEvent();
4554 			}
4555 		};
4556 		+/
4557 
4558 		win.onDestroyed = () {
4559 			this.dispose();
4560 		};
4561 
4562 		version(win32_widgets) {
4563 			Widget.nativeMapping[win.hwnd] = this;
4564 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4565 		} else {
4566 			win.setEventHandlers(
4567 				(MouseEvent e) {
4568 					Widget p = this;
4569 					while(p ! is parentWindow) {
4570 						e.x += p.x;
4571 						e.y += p.y;
4572 						p = p.parent;
4573 					}
4574 					parentWindow.dispatchMouseEvent(e);
4575 				},
4576 				(KeyEvent e) {
4577 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
4578 					parentWindow.dispatchKeyEvent(e);
4579 				},
4580 				(dchar e) {
4581 					parentWindow.dispatchCharEvent(e);
4582 				},
4583 			);
4584 		}
4585 
4586 	}
4587 
4588 	override bool showOrHideIfNativeWindow(bool shouldShow) {
4589 		auto cur = hidden;
4590 		win.hidden = !shouldShow;
4591 		if(cur != shouldShow && shouldShow)
4592 			redraw();
4593 		return true;
4594 	}
4595 
4596 	/// OpenGL widgets cannot have child widgets. Do not call this.
4597 	/* @disable */ final override void addChild(Widget, int) {
4598 		throw new Error("cannot add children to OpenGL widgets");
4599 	}
4600 
4601 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
4602 	/// Keep in mind that events like mouse coordinates are still relative to your size.
4603 	override void registerMovement() {
4604 		// writefln("%d %d %d %d", x,y,width,height);
4605 		version(win32_widgets)
4606 			auto pos = getChildPositionRelativeToParentHwnd(this);
4607 		else
4608 			auto pos = getChildPositionRelativeToParentOrigin(this);
4609 		win.moveResize(pos[0], pos[1], width, height);
4610 
4611 		registerMovementAdditionalWork();
4612 		sendResizeEvent();
4613 	}
4614 
4615 	abstract void registerMovementAdditionalWork();
4616 }
4617 
4618 /++
4619 	Nests an opengl capable window inside this window as a widget.
4620 
4621 	You may also just want to create an additional [SimpleWindow] with
4622 	[OpenGlOptions.yes] yourself.
4623 
4624 	An OpenGL widget cannot have child widgets. It will throw if you try.
4625 +/
4626 static if(OpenGlEnabled)
4627 class OpenGlWidget : NestedChildWindowWidget {
4628 
4629 	override void registerMovementAdditionalWork() {
4630 		win.setAsCurrentOpenGlContext();
4631 	}
4632 
4633 	///
4634 	this(Widget parent) {
4635 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4636 		super(win, parent);
4637 	}
4638 
4639 	override void paint(WidgetPainter painter) {
4640 		win.setAsCurrentOpenGlContext();
4641 		glViewport(0, 0, this.width, this.height);
4642 		win.redrawOpenGlSceneNow();
4643 	}
4644 
4645 	void redrawOpenGlScene(void delegate() dg) {
4646 		win.redrawOpenGlScene = dg;
4647 	}
4648 }
4649 
4650 /++
4651 	This demo shows how to draw text in an opengl scene.
4652 +/
4653 unittest {
4654 	import arsd.minigui;
4655 	import arsd.ttf;
4656 
4657 	void main() {
4658 		auto window = new Window();
4659 
4660 		auto widget = new OpenGlWidget(window);
4661 
4662 		// old means non-shader code so compatible with glBegin etc.
4663 		// tbh I haven't implemented new one in font yet...
4664 		// anyway, declaring here, will construct soon.
4665 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
4666 
4667 		// this is a little bit awkward, calling some methods through
4668 		// the underlying SimpleWindow `win` method, and you can't do this
4669 		// on a nanovega widget due to conflicts so I should probably fix
4670 		// the api to be a bit easier. But here it will work.
4671 		//
4672 		// Alternatively, you could load the font on the first draw, inside
4673 		// the redrawOpenGlScene, and keep a flag so you don't do it every
4674 		// time. That'd be a bit easier since the lib sets up the context
4675 		// by then guaranteed.
4676 		//
4677 		// But still, I wanna show this.
4678 		widget.win.visibleForTheFirstTime = delegate {
4679 			// must set the opengl context
4680 			widget.win.setAsCurrentOpenGlContext();
4681 
4682 			// if you were doing a OpenGL 3+ shader, this
4683 			// gets especially important to do in order. With
4684 			// old-style opengl, I think you can even do it
4685 			// in main(), but meh, let's show it more correctly.
4686 
4687 			// Anyway, now it is time to load the font from the
4688 			// OS (you can alternatively load one from a .ttf file
4689 			// you bundle with the application), then load the
4690 			// font into texture for drawing.
4691 
4692 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
4693 
4694 			assert(!osfont.isNull()); // make sure it actually loaded
4695 
4696 			// using typeof to avoid repeating the long name lol
4697 			glfont = new typeof(glfont)(
4698 				// get the raw data from the font for loading in here
4699 				// since it doesn't use the OS function to draw the
4700 				// text, we gotta treat it more as a file than as
4701 				// a drawing api.
4702 				osfont.getTtfBytes(),
4703 				18, // need to respecify size since opengl world is different coordinate system
4704 
4705 				// these last two numbers are why it is called
4706 				// "Limited" font. It only loads the characters
4707 				// in the given range, since the texture atlas
4708 				// it references is all a big image generated ahead
4709 				// of time. You could maybe do the whole thing but
4710 				// idk how much memory that is.
4711 				//
4712 				// But here, 0-128 represents the ASCII range, so
4713 				// good enough for most English things, numeric labels,
4714 				// etc.
4715 				0,
4716 				128
4717 			);
4718 		};
4719 
4720 		widget.redrawOpenGlScene = () {
4721 			// now we can use the glfont's drawString function
4722 
4723 			// first some opengl setup. You can do this in one place
4724 			// on window first visible too in many cases, just showing
4725 			// here cuz it is easier for me.
4726 
4727 			// gonna need some alpha blending or it just looks awful
4728 			glEnable(GL_BLEND);
4729 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
4730 			glClearColor(0,0,0,0);
4731 			glDepthFunc(GL_LEQUAL);
4732 
4733 			// Also need to enable 2d textures, since it draws the
4734 			// font characters as images baked in
4735 			glMatrixMode(GL_MODELVIEW);
4736 			glLoadIdentity();
4737 			glDisable(GL_DEPTH_TEST);
4738 			glEnable(GL_TEXTURE_2D);
4739 
4740 			// the orthographic matrix is best for 2d things like text
4741 			// so let's set that up. This matrix makes the coordinates
4742 			// in the opengl scene be one-to-one with the actual pixels
4743 			// on screen. (Not necessarily best, you may wish to scale
4744 			// things, but it does help keep fonts looking normal.)
4745 			glMatrixMode(GL_PROJECTION);
4746 			glLoadIdentity();
4747 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
4748 
4749 			// you can do other glScale, glRotate, glTranslate, etc
4750 			// to the matrix here of course if you want.
4751 
4752 			// note the x,y coordinates here are for the text baseline
4753 			// NOT the upper-left corner. The baseline is like the line
4754 			// in the notebook you write on. Most the letters are actually
4755 			// above it, but some, like p and q, dip a bit below it.
4756 			//
4757 			// So if you're used to the upper left coordinate like the
4758 			// rest of simpledisplay/minigui usually do, do the
4759 			// y + glfont.ascent to bring it down a little. So this
4760 			// example puts the string in the upper left of the window.
4761 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
4762 
4763 			// re color btw: the function sets a solid color internally,
4764 			// but you actually COULD do your own thing for rainbow effects
4765 			// and the sort if you wanted too, by pulling its guts out.
4766 			// Just view its source for an idea of how it actually draws:
4767 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
4768 
4769 			// it gets a bit complicated with the character positioning,
4770 			// but the opengl parts are fairly simple: bind a texture,
4771 			// set the color, draw a quad for each letter.
4772 
4773 
4774 			// the last optional argument there btw is a bounding box
4775 			// it will/ use to word wrap and return an object you can
4776 			// use to implement scrolling or pagination; it tells how
4777 			// much of the string didn't fit in the box. But for simple
4778 			// labels we can just ignore that.
4779 
4780 
4781 			// I'd suggest drawing text as the last step, after you
4782 			// do your other drawing. You might use the push/pop matrix
4783 			// stuff to keep your place. You, in theory, should be able
4784 			// to do text in a 3d space but I've never actually tried
4785 			// that....
4786 		};
4787 
4788 		window.loop();
4789 	}
4790 }
4791 
4792 version(custom_widgets)
4793 	private alias ListWidgetBase = ScrollableWidget;
4794 else
4795 	private alias ListWidgetBase = Widget;
4796 
4797 /++
4798 	A list widget contains a list of strings that the user can examine and select.
4799 
4800 
4801 	In the future, items in the list may be possible to be more than just strings.
4802 
4803 	See_Also:
4804 		[TableView]
4805 +/
4806 class ListWidget : ListWidgetBase {
4807 	/// 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.
4808 	mixin Emits!(ChangeEvent!void);
4809 
4810 	static struct Option {
4811 		string label;
4812 		bool selected;
4813 		void* tag;
4814 	}
4815 
4816 	/++
4817 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
4818 	+/
4819 	void setSelection(int y) {
4820 		if(!multiSelect)
4821 			foreach(ref opt; options)
4822 				opt.selected = false;
4823 		if(y >= 0 && y < options.length)
4824 			options[y].selected = !options[y].selected;
4825 
4826 		this.emit!(ChangeEvent!void)(delegate {});
4827 
4828 		version(custom_widgets)
4829 			redraw();
4830 	}
4831 
4832 	/++
4833 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
4834 		Returns -1 if nothing is selected.
4835 	+/
4836 	int getSelection()
4837 	{
4838 		foreach(i, opt; options) {
4839 			if (opt.selected)
4840 				return cast(int) i;
4841 		}
4842 		return -1;
4843 	}
4844 
4845 	version(custom_widgets)
4846 	override void defaultEventHandler_click(ClickEvent event) {
4847 		this.focus();
4848 		if(event.button == MouseButton.left) {
4849 			auto y = (event.clientY - 4) / defaultLineHeight;
4850 			if(y >= 0 && y < options.length) {
4851 				setSelection(y);
4852 			}
4853 		}
4854 		super.defaultEventHandler_click(event);
4855 	}
4856 
4857 	this(Widget parent) {
4858 		tabStop = false;
4859 		super(parent);
4860 		version(win32_widgets)
4861 			createWin32Window(this, WC_LISTBOX, "",
4862 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
4863 	}
4864 
4865 	version(win32_widgets)
4866 	override void handleWmCommand(ushort code, ushort id) {
4867 		switch(code) {
4868 			case LBN_SELCHANGE:
4869 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
4870 				setSelection(cast(int) sel);
4871 			break;
4872 			default:
4873 		}
4874 	}
4875 
4876 
4877 	version(custom_widgets)
4878 	override void paintFrameAndBackground(WidgetPainter painter) {
4879 		draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor);
4880 	}
4881 
4882 	version(custom_widgets)
4883 	override void paint(WidgetPainter painter) {
4884 		auto cs = getComputedStyle();
4885 		auto pos = Point(4, 4);
4886 		foreach(idx, option; options) {
4887 			painter.fillColor = painter.visualTheme.widgetBackgroundColor;
4888 			painter.outlineColor = painter.visualTheme.widgetBackgroundColor;
4889 			painter.drawRectangle(pos, width - 8, defaultLineHeight);
4890 			if(option.selected) {
4891 				//painter.rasterOp = RasterOp.xor;
4892 				painter.outlineColor = cs.selectionForegroundColor;
4893 				painter.fillColor = cs.selectionBackgroundColor;
4894 				painter.drawRectangle(pos, width - 8, defaultLineHeight);
4895 				//painter.rasterOp = RasterOp.normal;
4896 			}
4897 			painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor;
4898 			painter.drawText(pos, option.label);
4899 			pos.y += defaultLineHeight;
4900 		}
4901 	}
4902 
4903 	static class Style : Widget.Style {
4904 		override WidgetBackground background() {
4905 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
4906 		}
4907 	}
4908 	mixin OverrideStyle!Style;
4909 	//mixin Padding!q{2};
4910 
4911 	void addOption(string text, void* tag = null) {
4912 		options ~= Option(text, false, tag);
4913 		version(win32_widgets) {
4914 			WCharzBuffer buffer = WCharzBuffer(text);
4915 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
4916 		}
4917 		version(custom_widgets) {
4918 			setContentSize(width, cast(int) (options.length * defaultLineHeight));
4919 			redraw();
4920 		}
4921 	}
4922 
4923 	void clear() {
4924 		options = null;
4925 		version(win32_widgets) {
4926 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
4927 				{}
4928 
4929 		} else version(custom_widgets) {
4930 			scrollTo(Point(0, 0));
4931 			redraw();
4932 		}
4933 	}
4934 
4935 	Option[] options;
4936 	version(win32_widgets)
4937 		enum multiSelect = false; /// not implemented yet
4938 	else
4939 		bool multiSelect;
4940 
4941 	override int heightStretchiness() { return 6; }
4942 }
4943 
4944 
4945 
4946 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
4947 enum ScrollBarShowPolicy {
4948 	automatic, /// automatically show the scroll bar if it is necessary
4949 	never, /// never show the scroll bar (scrolling must be done programmatically)
4950 	always /// always show the scroll bar, even if it is disabled
4951 }
4952 
4953 /++
4954 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
4955 
4956 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
4957 +/
4958 // FIXME ScrollBarShowPolicy
4959 // FIXME: use the ScrollMessageWidget in here now that it exists
4960 class ScrollableWidget : Widget {
4961 	// FIXME: make line size configurable
4962 	// FIXME: add keyboard controls
4963 	version(win32_widgets) {
4964 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
4965 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
4966 				auto pos = HIWORD(wParam);
4967 				auto m = LOWORD(wParam);
4968 
4969 				// FIXME: I can reintroduce the
4970 				// scroll bars now by using this
4971 				// in the top-level window handler
4972 				// to forward comamnds
4973 				auto scrollbarHwnd = lParam;
4974 				switch(m) {
4975 					case SB_BOTTOM:
4976 						if(msg == WM_HSCROLL)
4977 							horizontalScrollTo(contentWidth_);
4978 						else
4979 							verticalScrollTo(contentHeight_);
4980 					break;
4981 					case SB_TOP:
4982 						if(msg == WM_HSCROLL)
4983 							horizontalScrollTo(0);
4984 						else
4985 							verticalScrollTo(0);
4986 					break;
4987 					case SB_ENDSCROLL:
4988 						// idk
4989 					break;
4990 					case SB_LINEDOWN:
4991 						if(msg == WM_HSCROLL)
4992 							horizontalScroll(scaleWithDpi(16));
4993 						else
4994 							verticalScroll(scaleWithDpi(16));
4995 					break;
4996 					case SB_LINEUP:
4997 						if(msg == WM_HSCROLL)
4998 							horizontalScroll(scaleWithDpi(-16));
4999 						else
5000 							verticalScroll(scaleWithDpi(-16));
5001 					break;
5002 					case SB_PAGEDOWN:
5003 						if(msg == WM_HSCROLL)
5004 							horizontalScroll(scaleWithDpi(100));
5005 						else
5006 							verticalScroll(scaleWithDpi(100));
5007 					break;
5008 					case SB_PAGEUP:
5009 						if(msg == WM_HSCROLL)
5010 							horizontalScroll(scaleWithDpi(-100));
5011 						else
5012 							verticalScroll(scaleWithDpi(-100));
5013 					break;
5014 					case SB_THUMBPOSITION:
5015 					case SB_THUMBTRACK:
5016 						if(msg == WM_HSCROLL)
5017 							horizontalScrollTo(pos);
5018 						else
5019 							verticalScrollTo(pos);
5020 
5021 						if(m == SB_THUMBTRACK) {
5022 							// the event loop doesn't seem to carry on with a requested redraw..
5023 							// so we request it to get our dirty bit set...
5024 							redraw();
5025 
5026 							// then we need to immediately actually redraw it too for instant feedback to user
5027 
5028 							SimpleWindow.processAllCustomEvents();
5029 							//if(parentWindow)
5030 								//parentWindow.actualRedraw();
5031 						}
5032 					break;
5033 					default:
5034 				}
5035 			}
5036 			return super.hookedWndProc(msg, wParam, lParam);
5037 		}
5038 	}
5039 	///
5040 	this(Widget parent) {
5041 		this.parentWindow = parent.parentWindow;
5042 
5043 		version(win32_widgets) {
5044 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
5045 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
5046 			super(parent);
5047 		} else version(custom_widgets) {
5048 			outerContainer = new InternalScrollableContainerWidget(this, parent);
5049 			super(outerContainer);
5050 		} else static assert(0);
5051 	}
5052 
5053 	version(custom_widgets)
5054 		InternalScrollableContainerWidget outerContainer;
5055 
5056 	override void defaultEventHandler_click(ClickEvent event) {
5057 		if(event.button == MouseButton.wheelUp)
5058 			verticalScroll(scaleWithDpi(-16));
5059 		if(event.button == MouseButton.wheelDown)
5060 			verticalScroll(scaleWithDpi(16));
5061 		super.defaultEventHandler_click(event);
5062 	}
5063 
5064 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5065 		switch(event.key) {
5066 			case Key.Left:
5067 				horizontalScroll(scaleWithDpi(-16));
5068 			break;
5069 			case Key.Right:
5070 				horizontalScroll(scaleWithDpi(16));
5071 			break;
5072 			case Key.Up:
5073 				verticalScroll(scaleWithDpi(-16));
5074 			break;
5075 			case Key.Down:
5076 				verticalScroll(scaleWithDpi(16));
5077 			break;
5078 			case Key.Home:
5079 				verticalScrollTo(0);
5080 			break;
5081 			case Key.End:
5082 				verticalScrollTo(contentHeight);
5083 			break;
5084 			case Key.PageUp:
5085 				verticalScroll(scaleWithDpi(-160));
5086 			break;
5087 			case Key.PageDown:
5088 				verticalScroll(scaleWithDpi(160));
5089 			break;
5090 			default:
5091 		}
5092 		super.defaultEventHandler_keydown(event);
5093 	}
5094 
5095 
5096 	version(win32_widgets)
5097 	override void recomputeChildLayout() {
5098 		super.recomputeChildLayout();
5099 		SCROLLINFO info;
5100 		info.cbSize = info.sizeof;
5101 		info.nPage = viewportHeight;
5102 		info.fMask = SIF_PAGE | SIF_RANGE;
5103 		info.nMin = 0;
5104 		info.nMax = contentHeight_;
5105 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5106 
5107 		info.cbSize = info.sizeof;
5108 		info.nPage = viewportWidth;
5109 		info.fMask = SIF_PAGE | SIF_RANGE;
5110 		info.nMin = 0;
5111 		info.nMax = contentWidth_;
5112 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5113 	}
5114 
5115 	/*
5116 		Scrolling
5117 		------------
5118 
5119 		You are assigned a width and a height by the layout engine, which
5120 		is your viewport box. However, you may draw more than that by setting
5121 		a contentWidth and contentHeight.
5122 
5123 		If these can be contained by the viewport, no scrollbar is displayed.
5124 		If they cannot fit though, it will automatically show scroll as necessary.
5125 
5126 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5127 		is zero, no vertical scrolling is performed.
5128 
5129 		If scrolling is necessary, the lib will automatically work with the bars.
5130 		When you redraw, the origin and clipping info in the painter is set so if
5131 		you just draw everything, it will work, but you can be more efficient by checking
5132 		the viewportWidth, viewportHeight, and scrollOrigin members.
5133 	*/
5134 
5135 	///
5136 	final @property int viewportWidth() {
5137 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5138 	}
5139 	///
5140 	final @property int viewportHeight() {
5141 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5142 	}
5143 
5144 	// FIXME property
5145 	Point scrollOrigin_;
5146 
5147 	///
5148 	final const(Point) scrollOrigin() {
5149 		return scrollOrigin_;
5150 	}
5151 
5152 	// the user sets these two
5153 	private int contentWidth_ = 0;
5154 	private int contentHeight_ = 0;
5155 
5156 	///
5157 	int contentWidth() { return contentWidth_; }
5158 	///
5159 	int contentHeight() { return contentHeight_; }
5160 
5161 	///
5162 	void setContentSize(int width, int height) {
5163 		contentWidth_ = width;
5164 		contentHeight_ = height;
5165 
5166 		version(custom_widgets) {
5167 			if(showingVerticalScroll || showingHorizontalScroll) {
5168 				outerContainer.queueRecomputeChildLayout();
5169 			}
5170 
5171 			if(showingVerticalScroll())
5172 				outerContainer.verticalScrollBar.redraw();
5173 			if(showingHorizontalScroll())
5174 				outerContainer.horizontalScrollBar.redraw();
5175 		} else version(win32_widgets) {
5176 			queueRecomputeChildLayout();
5177 		} else static assert(0);
5178 	}
5179 
5180 	///
5181 	void verticalScroll(int delta) {
5182 		verticalScrollTo(scrollOrigin.y + delta);
5183 	}
5184 	///
5185 	void verticalScrollTo(int pos) {
5186 		scrollOrigin_.y = pos;
5187 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5188 			scrollOrigin_.y = contentHeight - viewportHeight;
5189 
5190 		if(scrollOrigin_.y < 0)
5191 			scrollOrigin_.y = 0;
5192 
5193 		version(win32_widgets) {
5194 			SCROLLINFO info;
5195 			info.cbSize = info.sizeof;
5196 			info.fMask = SIF_POS;
5197 			info.nPos = scrollOrigin_.y;
5198 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5199 		} else version(custom_widgets) {
5200 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5201 		} else static assert(0);
5202 
5203 		redraw();
5204 	}
5205 
5206 	///
5207 	void horizontalScroll(int delta) {
5208 		horizontalScrollTo(scrollOrigin.x + delta);
5209 	}
5210 	///
5211 	void horizontalScrollTo(int pos) {
5212 		scrollOrigin_.x = pos;
5213 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5214 			scrollOrigin_.x = contentWidth - viewportWidth;
5215 
5216 		if(scrollOrigin_.x < 0)
5217 			scrollOrigin_.x = 0;
5218 
5219 		version(win32_widgets) {
5220 			SCROLLINFO info;
5221 			info.cbSize = info.sizeof;
5222 			info.fMask = SIF_POS;
5223 			info.nPos = scrollOrigin_.x;
5224 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5225 		} else version(custom_widgets) {
5226 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5227 		} else static assert(0);
5228 
5229 		redraw();
5230 	}
5231 	///
5232 	void scrollTo(Point p) {
5233 		verticalScrollTo(p.y);
5234 		horizontalScrollTo(p.x);
5235 	}
5236 
5237 	///
5238 	void ensureVisibleInScroll(Point p) {
5239 		auto rect = viewportRectangle();
5240 		if(rect.contains(p))
5241 			return;
5242 		if(p.x < rect.left)
5243 			horizontalScroll(p.x - rect.left);
5244 		else if(p.x > rect.right)
5245 			horizontalScroll(p.x - rect.right);
5246 
5247 		if(p.y < rect.top)
5248 			verticalScroll(p.y - rect.top);
5249 		else if(p.y > rect.bottom)
5250 			verticalScroll(p.y - rect.bottom);
5251 	}
5252 
5253 	///
5254 	void ensureVisibleInScroll(Rectangle rect) {
5255 		ensureVisibleInScroll(rect.upperLeft);
5256 		ensureVisibleInScroll(rect.lowerRight);
5257 	}
5258 
5259 	///
5260 	Rectangle viewportRectangle() {
5261 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5262 	}
5263 
5264 	///
5265 	bool showingHorizontalScroll() {
5266 		return contentWidth > width;
5267 	}
5268 	///
5269 	bool showingVerticalScroll() {
5270 		return contentHeight > height;
5271 	}
5272 
5273 	/// This is called before the ordinary paint delegate,
5274 	/// giving you a chance to draw the window frame, etc,
5275 	/// before the scroll clip takes effect
5276 	void paintFrameAndBackground(WidgetPainter painter) {
5277 		version(win32_widgets) {
5278 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5279 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5280 			// since the pen is null, to fill the whole space, we need the +1 on both.
5281 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5282 			SelectObject(painter.impl.hdc, p);
5283 			SelectObject(painter.impl.hdc, b);
5284 		}
5285 
5286 	}
5287 
5288 	// make space for the scroll bar, and that's it.
5289 	final override int paddingRight() { return scaleWithDpi(16); }
5290 	final override int paddingBottom() { return scaleWithDpi(16); }
5291 
5292 	/*
5293 		END SCROLLING
5294 	*/
5295 
5296 	override WidgetPainter draw() {
5297 		int x = this.x, y = this.y;
5298 		auto parent = this.parent;
5299 		while(parent) {
5300 			x += parent.x;
5301 			y += parent.y;
5302 			parent = parent.parent;
5303 		}
5304 
5305 		//version(win32_widgets) {
5306 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5307 		//} else {
5308 			auto painter = parentWindow.win.draw(true);
5309 		//}
5310 		painter.originX = x;
5311 		painter.originY = y;
5312 
5313 		painter.originX = painter.originX - scrollOrigin.x;
5314 		painter.originY = painter.originY - scrollOrigin.y;
5315 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
5316 
5317 		return WidgetPainter(painter, this);
5318 	}
5319 
5320 	mixin ScrollableChildren;
5321 }
5322 
5323 // you need to have a Point scrollOrigin in the class somewhere
5324 // and a paintFrameAndBackground
5325 private mixin template ScrollableChildren() {
5326 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5327 		if(hidden)
5328 			return;
5329 
5330 		//version(win32_widgets)
5331 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5332 
5333 		painter.originX = lox + x;
5334 		painter.originY = loy + y;
5335 
5336 		bool actuallyPainted = false;
5337 
5338 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
5339 		if(clip == Rectangle.init)
5340 			return;
5341 
5342 		if(force || redrawRequested) {
5343 			//painter.setClipRectangle(scrollOrigin, width, height);
5344 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5345 			paintFrameAndBackground(painter);
5346 		}
5347 
5348 		/+
5349 		version(win32_widgets) {
5350 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
5351 		}
5352 		+/
5353 
5354 		painter.originX = painter.originX - scrollOrigin.x;
5355 		painter.originY = painter.originY - scrollOrigin.y;
5356 		if(force || redrawRequested) {
5357 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
5358 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
5359 
5360 			//erase(painter); // we paintFrameAndBackground above so no need
5361 			if(painter.visualTheme)
5362 				painter.visualTheme.doPaint(this, painter);
5363 			else
5364 				paint(painter);
5365 
5366 			if(invalidate) {
5367 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5368 				// children are contained inside this, so no need to do extra work
5369 				invalidate = false;
5370 			}
5371 
5372 
5373 			actuallyPainted = true;
5374 			redrawRequested = false;
5375 		}
5376 
5377 		foreach(child; children) {
5378 			if(cast(FixedPosition) child)
5379 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5380 			else
5381 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5382 		}
5383 	}
5384 }
5385 
5386 private class InternalScrollableContainerInsideWidget : ContainerWidget {
5387 	ScrollableContainerWidget scw;
5388 
5389 	this(ScrollableContainerWidget parent) {
5390 		scw = parent;
5391 		super(parent);
5392 	}
5393 
5394 	version(custom_widgets)
5395 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5396 		if(hidden)
5397 			return;
5398 
5399 		bool actuallyPainted = false;
5400 
5401 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
5402 
5403 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
5404 		if(clip == Rectangle.init)
5405 			return;
5406 
5407 		painter.originX = lox + x - scrollOrigin.x;
5408 		painter.originY = loy + y - scrollOrigin.y;
5409 		if(force || redrawRequested) {
5410 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5411 
5412 			erase(painter);
5413 			if(painter.visualTheme)
5414 				painter.visualTheme.doPaint(this, painter);
5415 			else
5416 				paint(painter);
5417 
5418 			if(invalidate) {
5419 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5420 				// children are contained inside this, so no need to do extra work
5421 				invalidate = false;
5422 			}
5423 
5424 			actuallyPainted = true;
5425 			redrawRequested = false;
5426 		}
5427 		foreach(child; children) {
5428 			if(cast(FixedPosition) child)
5429 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5430 			else
5431 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5432 		}
5433 	}
5434 
5435 	version(custom_widgets)
5436 	override protected void addScrollPosition(ref int x, ref int y) {
5437 		x += scw.scrollX_;
5438 		y += scw.scrollY_;
5439 	}
5440 }
5441 
5442 /++
5443 	A widget meant to contain other widgets that may need to scroll.
5444 
5445 	Currently buggy.
5446 
5447 	History:
5448 		Added July 1, 2021 (dub v10.2)
5449 
5450 		On January 3, 2022, I tried to use it in a few other cases
5451 		and found it only worked well in the original test case. Since
5452 		it still sucks, I think I'm going to rewrite it again.
5453 +/
5454 class ScrollableContainerWidget : ContainerWidget {
5455 	///
5456 	this(Widget parent) {
5457 		super(parent);
5458 
5459 		container = new InternalScrollableContainerInsideWidget(this);
5460 		hsb = new HorizontalScrollbar(this);
5461 		vsb = new VerticalScrollbar(this);
5462 
5463 		tabStop = false;
5464 		container.tabStop = false;
5465 		magic = true;
5466 
5467 
5468 		vsb.addEventListener("scrolltonextline", () {
5469 			scrollBy(0, scaleWithDpi(16));
5470 		});
5471 		vsb.addEventListener("scrolltopreviousline", () {
5472 			scrollBy(0,scaleWithDpi( -16));
5473 		});
5474 		vsb.addEventListener("scrolltonextpage", () {
5475 			scrollBy(0, container.height);
5476 		});
5477 		vsb.addEventListener("scrolltopreviouspage", () {
5478 			scrollBy(0, -container.height);
5479 		});
5480 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
5481 			scrollTo(scrollX_, spe.value);
5482 		});
5483 
5484 		this.addEventListener(delegate (scope ClickEvent e) {
5485 			if(e.button == MouseButton.wheelUp) {
5486 				if(!e.defaultPrevented)
5487 					scrollBy(0, scaleWithDpi(-16));
5488 				e.stopPropagation();
5489 			} else if(e.button == MouseButton.wheelDown) {
5490 				if(!e.defaultPrevented)
5491 					scrollBy(0, scaleWithDpi(16));
5492 				e.stopPropagation();
5493 			}
5494 		});
5495 	}
5496 
5497 	/+
5498 	override void defaultEventHandler_click(ClickEvent e) {
5499 	}
5500 	+/
5501 
5502 	override void removeAllChildren() {
5503 		container.removeAllChildren();
5504 	}
5505 
5506 	void scrollTo(int x, int y) {
5507 		scrollBy(x - scrollX_, y - scrollY_);
5508 	}
5509 
5510 	void scrollBy(int x, int y) {
5511 		auto ox = scrollX_;
5512 		auto oy = scrollY_;
5513 
5514 		auto nx = ox + x;
5515 		auto ny = oy + y;
5516 
5517 		if(nx < 0)
5518 			nx = 0;
5519 		if(ny < 0)
5520 			ny = 0;
5521 
5522 		auto maxX = hsb.max - container.width;
5523 		if(maxX < 0) maxX = 0;
5524 		auto maxY = vsb.max - container.height;
5525 		if(maxY < 0) maxY = 0;
5526 
5527 		if(nx > maxX)
5528 			nx = maxX;
5529 		if(ny > maxY)
5530 			ny = maxY;
5531 
5532 		auto dx = nx - ox;
5533 		auto dy = ny - oy;
5534 
5535 		if(dx || dy) {
5536 			version(win32_widgets)
5537 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
5538 			else {
5539 				redraw();
5540 			}
5541 
5542 			hsb.setPosition = nx;
5543 			vsb.setPosition = ny;
5544 
5545 			scrollX_ = nx;
5546 			scrollY_ = ny;
5547 		}
5548 	}
5549 
5550 	private int scrollX_;
5551 	private int scrollY_;
5552 
5553 	void setTotalArea(int width, int height) {
5554 		hsb.setMax(width);
5555 		vsb.setMax(height);
5556 	}
5557 
5558 	///
5559 	void setViewableArea(int width, int height) {
5560 		hsb.setViewableArea(width);
5561 		vsb.setViewableArea(height);
5562 	}
5563 
5564 	private bool magic;
5565 	override void addChild(Widget w, int position = int.max) {
5566 		if(magic)
5567 			container.addChild(w, position);
5568 		else
5569 			super.addChild(w, position);
5570 	}
5571 
5572 	override void recomputeChildLayout() {
5573 		if(hsb is null || vsb is null || container is null) return;
5574 
5575 		/+
5576 		writeln(x, " ", y , " ", width, " ", height);
5577 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
5578 		+/
5579 
5580 		registerMovement();
5581 
5582 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
5583 		hsb.x = 0;
5584 		hsb.y = this.height - hsb.height;
5585 		hsb.width = this.width - scaleWithDpi(16);
5586 		hsb.recomputeChildLayout();
5587 
5588 		vsb.width = scaleWithDpi(16); // FIXME?
5589 		vsb.x = this.width - vsb.width;
5590 		vsb.y = 0;
5591 		vsb.height = this.height - scaleWithDpi(16);
5592 		vsb.recomputeChildLayout();
5593 
5594 		container.x = 0;
5595 		container.y = 0;
5596 		container.width = this.width - vsb.width;
5597 		container.height = this.height - hsb.height;
5598 		container.recomputeChildLayout();
5599 
5600 		scrollX_ = 0;
5601 		scrollY_ = 0;
5602 
5603 		hsb.setPosition(0);
5604 		vsb.setPosition(0);
5605 
5606 		int mw, mh;
5607 		Widget c = container;
5608 		// FIXME: hack here to handle a layout inside...
5609 		if(c.children.length == 1 && cast(Layout) c.children[0])
5610 			c = c.children[0];
5611 		foreach(child; c.children) {
5612 			auto w = child.x + child.width;
5613 			auto h = child.y + child.height;
5614 
5615 			if(w > mw) mw = w;
5616 			if(h > mh) mh = h;
5617 		}
5618 
5619 		setTotalArea(mw, mh);
5620 		setViewableArea(width, height);
5621 	}
5622 
5623 	override int minHeight() { return scaleWithDpi(64); }
5624 
5625 	HorizontalScrollbar hsb;
5626 	VerticalScrollbar vsb;
5627 	ContainerWidget container;
5628 }
5629 
5630 
5631 version(custom_widgets)
5632 private class InternalScrollableContainerWidget : Widget {
5633 
5634 	ScrollableWidget sw;
5635 
5636 	VerticalScrollbar verticalScrollBar;
5637 	HorizontalScrollbar horizontalScrollBar;
5638 
5639 	this(ScrollableWidget sw, Widget parent) {
5640 		this.sw = sw;
5641 
5642 		this.tabStop = false;
5643 
5644 		super(parent);
5645 
5646 		horizontalScrollBar = new HorizontalScrollbar(this);
5647 		verticalScrollBar = new VerticalScrollbar(this);
5648 
5649 		horizontalScrollBar.showing_ = false;
5650 		verticalScrollBar.showing_ = false;
5651 
5652 		horizontalScrollBar.addEventListener("scrolltonextline", {
5653 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
5654 			sw.horizontalScrollTo(horizontalScrollBar.position);
5655 		});
5656 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
5657 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
5658 			sw.horizontalScrollTo(horizontalScrollBar.position);
5659 		});
5660 		verticalScrollBar.addEventListener("scrolltonextline", {
5661 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
5662 			sw.verticalScrollTo(verticalScrollBar.position);
5663 		});
5664 		verticalScrollBar.addEventListener("scrolltopreviousline", {
5665 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
5666 			sw.verticalScrollTo(verticalScrollBar.position);
5667 		});
5668 		horizontalScrollBar.addEventListener("scrolltonextpage", {
5669 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
5670 			sw.horizontalScrollTo(horizontalScrollBar.position);
5671 		});
5672 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
5673 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
5674 			sw.horizontalScrollTo(horizontalScrollBar.position);
5675 		});
5676 		verticalScrollBar.addEventListener("scrolltonextpage", {
5677 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
5678 			sw.verticalScrollTo(verticalScrollBar.position);
5679 		});
5680 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
5681 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
5682 			sw.verticalScrollTo(verticalScrollBar.position);
5683 		});
5684 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
5685 			horizontalScrollBar.setPosition(event.intValue);
5686 			sw.horizontalScrollTo(horizontalScrollBar.position);
5687 		});
5688 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
5689 			verticalScrollBar.setPosition(event.intValue);
5690 			sw.verticalScrollTo(verticalScrollBar.position);
5691 		});
5692 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
5693 			horizontalScrollBar.setPosition(event.intValue);
5694 			sw.horizontalScrollTo(horizontalScrollBar.position);
5695 		});
5696 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
5697 			verticalScrollBar.setPosition(event.intValue);
5698 		});
5699 	}
5700 
5701 	// this is supposed to be basically invisible...
5702 	override int minWidth() { return sw.minWidth; }
5703 	override int minHeight() { return sw.minHeight; }
5704 	override int maxWidth() { return sw.maxWidth; }
5705 	override int maxHeight() { return sw.maxHeight; }
5706 	override int widthStretchiness() { return sw.widthStretchiness; }
5707 	override int heightStretchiness() { return sw.heightStretchiness; }
5708 	override int marginLeft() { return sw.marginLeft; }
5709 	override int marginRight() { return sw.marginRight; }
5710 	override int marginTop() { return sw.marginTop; }
5711 	override int marginBottom() { return sw.marginBottom; }
5712 	override int paddingLeft() { return sw.paddingLeft; }
5713 	override int paddingRight() { return sw.paddingRight; }
5714 	override int paddingTop() { return sw.paddingTop; }
5715 	override int paddingBottom() { return sw.paddingBottom; }
5716 	override void focus() { sw.focus(); }
5717 
5718 
5719 	override void recomputeChildLayout() {
5720 		// The stupid thing needs to calculate if a scroll bar is needed...
5721 		recomputeChildLayoutHelper();
5722 		// then running it again will position things correctly if the bar is NOT needed
5723 		recomputeChildLayoutHelper();
5724 
5725 		// this sucks but meh it barely works
5726 	}
5727 
5728 	private void recomputeChildLayoutHelper() {
5729 		if(sw is null) return;
5730 
5731 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
5732 		if(horizontalScrollBar && verticalScrollBar) {
5733 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
5734 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
5735 			horizontalScrollBar.x = 0;
5736 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
5737 
5738 			verticalScrollBar.width = verticalScrollBar.minWidth();
5739 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
5740 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
5741 			verticalScrollBar.y = 0 + 2;
5742 
5743 			sw.x = 0;
5744 			sw.y = 0;
5745 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
5746 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
5747 
5748 			if(sw.contentWidth_ <= this.width)
5749 				sw.scrollOrigin_.x = 0;
5750 			if(sw.contentHeight_ <= this.height)
5751 				sw.scrollOrigin_.y = 0;
5752 
5753 			horizontalScrollBar.recomputeChildLayout();
5754 			verticalScrollBar.recomputeChildLayout();
5755 			sw.recomputeChildLayout();
5756 		}
5757 
5758 		if(sw.contentWidth_ <= this.width)
5759 			sw.scrollOrigin_.x = 0;
5760 		if(sw.contentHeight_ <= this.height)
5761 			sw.scrollOrigin_.y = 0;
5762 
5763 		if(sw.showingHorizontalScroll())
5764 			horizontalScrollBar.showing(true, false);
5765 		else
5766 			horizontalScrollBar.showing(false, false);
5767 		if(sw.showingVerticalScroll())
5768 			verticalScrollBar.showing(true, false);
5769 		else
5770 			verticalScrollBar.showing(false, false);
5771 
5772 		verticalScrollBar.setViewableArea(sw.viewportHeight());
5773 		verticalScrollBar.setMax(sw.contentHeight);
5774 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
5775 
5776 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
5777 		horizontalScrollBar.setMax(sw.contentWidth);
5778 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
5779 	}
5780 }
5781 
5782 /*
5783 class ScrollableClientWidget : Widget {
5784 	this(Widget parent) {
5785 		super(parent);
5786 	}
5787 	override void paint(WidgetPainter p) {
5788 		parent.paint(p);
5789 	}
5790 }
5791 */
5792 
5793 /++
5794 	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.
5795 +/
5796 abstract class Slider : Widget {
5797 	this(int min, int max, int step, Widget parent) {
5798 		min_ = min;
5799 		max_ = max;
5800 		step_ = step;
5801 		page_ = step;
5802 		super(parent);
5803 	}
5804 
5805 	private int min_;
5806 	private int max_;
5807 	private int step_;
5808 	private int position_;
5809 	private int page_;
5810 
5811 	// selection start and selection end
5812 	// tics
5813 	// tooltip?
5814 	// some way to see and just type the value
5815 	// win32 buddy controls are labels
5816 
5817 	///
5818 	void setMin(int a) {
5819 		min_ = a;
5820 		version(custom_widgets)
5821 			redraw();
5822 		version(win32_widgets)
5823 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
5824 	}
5825 	///
5826 	int min() {
5827 		return min_;
5828 	}
5829 	///
5830 	void setMax(int a) {
5831 		max_ = a;
5832 		version(custom_widgets)
5833 			redraw();
5834 		version(win32_widgets)
5835 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
5836 	}
5837 	///
5838 	int max() {
5839 		return max_;
5840 	}
5841 	///
5842 	void setPosition(int a) {
5843 		if(a > max)
5844 			a = max;
5845 		if(a < min)
5846 			a = min;
5847 		position_ = a;
5848 		version(custom_widgets)
5849 			setPositionCustom(a);
5850 
5851 		version(win32_widgets)
5852 			setPositionWindows(a);
5853 	}
5854 	version(win32_widgets) {
5855 		protected abstract void setPositionWindows(int a);
5856 	}
5857 
5858 	protected abstract int win32direction();
5859 
5860 	/++
5861 		Alias for [position] for better compatibility with generic code.
5862 
5863 		History:
5864 			Added October 5, 2021
5865 	+/
5866 	@property int value() {
5867 		return position;
5868 	}
5869 
5870 	///
5871 	int position() {
5872 		return position_;
5873 	}
5874 	///
5875 	void setStep(int a) {
5876 		step_ = a;
5877 		version(win32_widgets)
5878 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
5879 	}
5880 	///
5881 	int step() {
5882 		return step_;
5883 	}
5884 	///
5885 	void setPageSize(int a) {
5886 		page_ = a;
5887 		version(win32_widgets)
5888 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
5889 	}
5890 	///
5891 	int pageSize() {
5892 		return page_;
5893 	}
5894 
5895 	private void notify() {
5896 		auto event = new ChangeEvent!int(this, &this.position);
5897 		event.dispatch();
5898 	}
5899 
5900 	version(win32_widgets)
5901 	void win32Setup(int style) {
5902 		createWin32Window(this, TRACKBAR_CLASS, "",
5903 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
5904 
5905 		// the trackbar sends the same messages as scroll, which
5906 		// our other layer sends as these... just gonna translate
5907 		// here
5908 		this.addDirectEventListener("scrolltoposition", (Event event) {
5909 			event.stopPropagation();
5910 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
5911 			notify();
5912 		});
5913 		this.addDirectEventListener("scrolltonextline", (Event event) {
5914 			event.stopPropagation();
5915 			this.setPosition(this.position + this.step_ * this.win32direction);
5916 			notify();
5917 		});
5918 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
5919 			event.stopPropagation();
5920 			this.setPosition(this.position - this.step_ * this.win32direction);
5921 			notify();
5922 		});
5923 		this.addDirectEventListener("scrolltonextpage", (Event event) {
5924 			event.stopPropagation();
5925 			this.setPosition(this.position + this.page_ * this.win32direction);
5926 			notify();
5927 		});
5928 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
5929 			event.stopPropagation();
5930 			this.setPosition(this.position - this.page_ * this.win32direction);
5931 			notify();
5932 		});
5933 
5934 		setMin(min_);
5935 		setMax(max_);
5936 		setStep(step_);
5937 		setPageSize(page_);
5938 	}
5939 
5940 	version(custom_widgets) {
5941 		protected MouseTrackingWidget thumb;
5942 
5943 		protected abstract void setPositionCustom(int a);
5944 
5945 		override void defaultEventHandler_keydown(KeyDownEvent event) {
5946 			switch(event.key) {
5947 				case Key.Up:
5948 				case Key.Right:
5949 					setPosition(position() - step() * win32direction);
5950 					changed();
5951 				break;
5952 				case Key.Down:
5953 				case Key.Left:
5954 					setPosition(position() + step() * win32direction);
5955 					changed();
5956 				break;
5957 				case Key.Home:
5958 					setPosition(win32direction > 0 ? min() : max());
5959 					changed();
5960 				break;
5961 				case Key.End:
5962 					setPosition(win32direction > 0 ? max() : min());
5963 					changed();
5964 				break;
5965 				case Key.PageUp:
5966 					setPosition(position() - pageSize() * win32direction);
5967 					changed();
5968 				break;
5969 				case Key.PageDown:
5970 					setPosition(position() + pageSize() * win32direction);
5971 					changed();
5972 				break;
5973 				default:
5974 			}
5975 			super.defaultEventHandler_keydown(event);
5976 		}
5977 
5978 		protected void changed() {
5979 			auto ev = new ChangeEvent!int(this, &position);
5980 			ev.dispatch();
5981 		}
5982 	}
5983 }
5984 
5985 /++
5986 
5987 +/
5988 class VerticalSlider : Slider {
5989 	this(int min, int max, int step, Widget parent) {
5990 		version(custom_widgets)
5991 			initialize();
5992 
5993 		super(min, max, step, parent);
5994 
5995 		version(win32_widgets)
5996 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
5997 	}
5998 
5999 	protected override int win32direction() {
6000 		return -1;
6001 	}
6002 
6003 	version(win32_widgets)
6004 	protected override void setPositionWindows(int a) {
6005 		// the windows thing makes the top 0 and i don't like that.
6006 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
6007 	}
6008 
6009 	version(custom_widgets)
6010 	private void initialize() {
6011 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
6012 
6013 		thumb.tabStop = false;
6014 
6015 		thumb.thumbWidth = width;
6016 		thumb.thumbHeight = scaleWithDpi(16);
6017 
6018 		thumb.addEventListener(EventType.change, () {
6019 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
6020 			sx = max - sx;
6021 			//informProgramThatUserChangedPosition(sx);
6022 
6023 			position_ = sx;
6024 
6025 			changed();
6026 		});
6027 	}
6028 
6029 	version(custom_widgets)
6030 	override void recomputeChildLayout() {
6031 		thumb.thumbWidth = this.width;
6032 		super.recomputeChildLayout();
6033 		setPositionCustom(position_);
6034 	}
6035 
6036 	version(custom_widgets)
6037 	protected override void setPositionCustom(int a) {
6038 		if(max())
6039 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
6040 		redraw();
6041 	}
6042 }
6043 
6044 /++
6045 
6046 +/
6047 class HorizontalSlider : Slider {
6048 	this(int min, int max, int step, Widget parent) {
6049 		version(custom_widgets)
6050 			initialize();
6051 
6052 		super(min, max, step, parent);
6053 
6054 		version(win32_widgets)
6055 			win32Setup(TBS_HORZ);
6056 	}
6057 
6058 	version(win32_widgets)
6059 	protected override void setPositionWindows(int a) {
6060 		SendMessage(hwnd, TBM_SETPOS, true, a);
6061 	}
6062 
6063 	protected override int win32direction() {
6064 		return 1;
6065 	}
6066 
6067 	version(custom_widgets)
6068 	private void initialize() {
6069 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6070 
6071 		thumb.tabStop = false;
6072 
6073 		thumb.thumbWidth = scaleWithDpi(16);
6074 		thumb.thumbHeight = height;
6075 
6076 		thumb.addEventListener(EventType.change, () {
6077 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6078 			//informProgramThatUserChangedPosition(sx);
6079 
6080 			position_ = sx;
6081 
6082 			changed();
6083 		});
6084 	}
6085 
6086 	version(custom_widgets)
6087 	override void recomputeChildLayout() {
6088 		thumb.thumbHeight = this.height;
6089 		super.recomputeChildLayout();
6090 		setPositionCustom(position_);
6091 	}
6092 
6093 	version(custom_widgets)
6094 	protected override void setPositionCustom(int a) {
6095 		if(max())
6096 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6097 		redraw();
6098 	}
6099 }
6100 
6101 
6102 ///
6103 abstract class ScrollbarBase : Widget {
6104 	///
6105 	this(Widget parent) {
6106 		super(parent);
6107 		tabStop = false;
6108 		step_ = scaleWithDpi(16);
6109 	}
6110 
6111 	private int viewableArea_;
6112 	private int max_;
6113 	private int step_;// = 16;
6114 	private int position_;
6115 
6116 	///
6117 	bool atEnd() {
6118 		return position_ + viewableArea_ >= max_;
6119 	}
6120 
6121 	///
6122 	bool atStart() {
6123 		return position_ == 0;
6124 	}
6125 
6126 	///
6127 	void setViewableArea(int a) {
6128 		viewableArea_ = a;
6129 		version(custom_widgets)
6130 			redraw();
6131 	}
6132 	///
6133 	void setMax(int a) {
6134 		max_ = a;
6135 		version(custom_widgets)
6136 			redraw();
6137 	}
6138 	///
6139 	int max() {
6140 		return max_;
6141 	}
6142 	///
6143 	void setPosition(int a) {
6144 		auto logicalMax = max_ - viewableArea_;
6145 		if(a == int.max)
6146 			a = logicalMax;
6147 
6148 		if(a > logicalMax)
6149 			a = logicalMax;
6150 		if(a < 0)
6151 			a = 0;
6152 
6153 		position_ = a;
6154 
6155 		version(custom_widgets)
6156 			redraw();
6157 	}
6158 	///
6159 	int position() {
6160 		return position_;
6161 	}
6162 	///
6163 	void setStep(int a) {
6164 		step_ = a;
6165 	}
6166 	///
6167 	int step() {
6168 		return step_;
6169 	}
6170 
6171 	// FIXME: remove this.... maybe
6172 	/+
6173 	protected void informProgramThatUserChangedPosition(int n) {
6174 		position_ = n;
6175 		auto evt = new Event(EventType.change, this);
6176 		evt.intValue = n;
6177 		evt.dispatch();
6178 	}
6179 	+/
6180 
6181 	version(custom_widgets) {
6182 		enum MIN_THUMB_SIZE = 8;
6183 
6184 		abstract protected int getBarDim();
6185 		int thumbSize() {
6186 			if(viewableArea_ >= max_ || max_ == 0)
6187 				return getBarDim();
6188 
6189 			int res = viewableArea_ * getBarDim() / max_;
6190 
6191 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6192 				res = scaleWithDpi(MIN_THUMB_SIZE);
6193 
6194 			return res;
6195 		}
6196 
6197 		int thumbPosition() {
6198 			/*
6199 				viewableArea_ is the viewport height/width
6200 				position_ is where we are
6201 			*/
6202 			//if(position_ + viewableArea_ >= max_)
6203 				//return getBarDim - thumbSize;
6204 
6205 			auto maximumPossibleValue = getBarDim() - thumbSize;
6206 			auto maximiumLogicalValue = max_ - viewableArea_;
6207 
6208 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6209 
6210 			return p;
6211 		}
6212 	}
6213 }
6214 
6215 //public import mgt;
6216 
6217 /++
6218 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6219 
6220 	Concrete subclasses may include a scrollbar thumb and a volume control.
6221 +/
6222 //version(custom_widgets)
6223 class MouseTrackingWidget : Widget {
6224 
6225 	///
6226 	int positionX() { return positionX_; }
6227 	///
6228 	int positionY() { return positionY_; }
6229 
6230 	///
6231 	void positionX(int p) { positionX_ = p; }
6232 	///
6233 	void positionY(int p) { positionY_ = p; }
6234 
6235 	private int positionX_;
6236 	private int positionY_;
6237 
6238 	///
6239 	enum Orientation {
6240 		horizontal, ///
6241 		vertical, ///
6242 		twoDimensional, ///
6243 	}
6244 
6245 	private int thumbWidth_;
6246 	private int thumbHeight_;
6247 
6248 	///
6249 	int thumbWidth() { return thumbWidth_; }
6250 	///
6251 	int thumbHeight() { return thumbHeight_; }
6252 	///
6253 	int thumbWidth(int a) { return thumbWidth_ = a; }
6254 	///
6255 	int thumbHeight(int a) { return thumbHeight_ = a; }
6256 
6257 	private bool dragging;
6258 	private bool hovering;
6259 	private int startMouseX, startMouseY;
6260 
6261 	///
6262 	this(Orientation orientation, Widget parent) {
6263 		super(parent);
6264 
6265 		//assert(parentWindow !is null);
6266 
6267 		addEventListener((MouseDownEvent event) {
6268 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6269 				dragging = true;
6270 				startMouseX = event.clientX - positionX;
6271 				startMouseY = event.clientY - positionY;
6272 				parentWindow.captureMouse(this);
6273 			} else {
6274 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6275 					positionX = event.clientX - thumbWidth / 2;
6276 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6277 					positionY = event.clientY - thumbHeight / 2;
6278 
6279 				if(positionX + thumbWidth > this.width)
6280 					positionX = this.width - thumbWidth;
6281 				if(positionY + thumbHeight > this.height)
6282 					positionY = this.height - thumbHeight;
6283 
6284 				if(positionX < 0)
6285 					positionX = 0;
6286 				if(positionY < 0)
6287 					positionY = 0;
6288 
6289 
6290 				// this.emit!(ChangeEvent!void)();
6291 				auto evt = new Event(EventType.change, this);
6292 				evt.sendDirectly();
6293 
6294 				redraw();
6295 
6296 			}
6297 		});
6298 
6299 		addEventListener(EventType.mouseup, (Event event) {
6300 			dragging = false;
6301 			parentWindow.releaseMouseCapture();
6302 		});
6303 
6304 		addEventListener(EventType.mouseout, (Event event) {
6305 			if(!hovering)
6306 				return;
6307 			hovering = false;
6308 			redraw();
6309 		});
6310 
6311 		int lpx, lpy;
6312 
6313 		addEventListener((MouseMoveEvent event) {
6314 			auto oh = hovering;
6315 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6316 				hovering = true;
6317 			} else {
6318 				hovering = false;
6319 			}
6320 			if(!dragging) {
6321 				if(hovering != oh)
6322 					redraw();
6323 				return;
6324 			}
6325 
6326 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6327 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
6328 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6329 				positionY = event.clientY - startMouseY;
6330 
6331 			if(positionX + thumbWidth > this.width)
6332 				positionX = this.width - thumbWidth;
6333 			if(positionY + thumbHeight > this.height)
6334 				positionY = this.height - thumbHeight;
6335 
6336 			if(positionX < 0)
6337 				positionX = 0;
6338 			if(positionY < 0)
6339 				positionY = 0;
6340 
6341 			if(positionX != lpx || positionY != lpy) {
6342 				lpx = positionX;
6343 				lpy = positionY;
6344 
6345 				auto evt = new Event(EventType.change, this);
6346 				evt.sendDirectly();
6347 			}
6348 
6349 			redraw();
6350 		});
6351 	}
6352 
6353 	version(custom_widgets)
6354 	override void paint(WidgetPainter painter) {
6355 		auto cs = getComputedStyle();
6356 		auto c = darken(cs.windowBackgroundColor, 0.2);
6357 		painter.outlineColor = c;
6358 		painter.fillColor = c;
6359 		painter.drawRectangle(Point(0, 0), this.width, this.height);
6360 
6361 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
6362 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
6363 	}
6364 }
6365 
6366 //version(custom_widgets)
6367 //private
6368 class HorizontalScrollbar : ScrollbarBase {
6369 
6370 	version(custom_widgets) {
6371 		private MouseTrackingWidget thumb;
6372 
6373 		override int getBarDim() {
6374 			return thumb.width;
6375 		}
6376 	}
6377 
6378 	override void setViewableArea(int a) {
6379 		super.setViewableArea(a);
6380 
6381 		version(win32_widgets) {
6382 			SCROLLINFO info;
6383 			info.cbSize = info.sizeof;
6384 			info.nPage = a + 1;
6385 			info.fMask = SIF_PAGE;
6386 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6387 		} else version(custom_widgets) {
6388 			thumb.positionX = thumbPosition;
6389 			thumb.thumbWidth = thumbSize;
6390 			thumb.redraw();
6391 		} else static assert(0);
6392 
6393 	}
6394 
6395 	override void setMax(int a) {
6396 		super.setMax(a);
6397 		version(win32_widgets) {
6398 			SCROLLINFO info;
6399 			info.cbSize = info.sizeof;
6400 			info.nMin = 0;
6401 			info.nMax = max;
6402 			info.fMask = SIF_RANGE;
6403 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6404 		} else version(custom_widgets) {
6405 			thumb.positionX = thumbPosition;
6406 			thumb.thumbWidth = thumbSize;
6407 			thumb.redraw();
6408 		}
6409 	}
6410 
6411 	override void setPosition(int a) {
6412 		super.setPosition(a);
6413 		version(win32_widgets) {
6414 			SCROLLINFO info;
6415 			info.cbSize = info.sizeof;
6416 			info.fMask = SIF_POS;
6417 			info.nPos = position;
6418 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6419 		} else version(custom_widgets) {
6420 			thumb.positionX = thumbPosition();
6421 			thumb.thumbWidth = thumbSize;
6422 			thumb.redraw();
6423 		} else static assert(0);
6424 	}
6425 
6426 	this(Widget parent) {
6427 		super(parent);
6428 
6429 		version(win32_widgets) {
6430 			createWin32Window(this, "Scrollbar"w, "",
6431 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
6432 		} else version(custom_widgets) {
6433 			auto vl = new HorizontalLayout(this);
6434 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
6435 			leftButton.setClickRepeat(scrollClickRepeatInterval);
6436 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
6437 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
6438 			rightButton.setClickRepeat(scrollClickRepeatInterval);
6439 
6440 			leftButton.tabStop = false;
6441 			rightButton.tabStop = false;
6442 			thumb.tabStop = false;
6443 
6444 			leftButton.addEventListener(EventType.triggered, () {
6445 				this.emitCommand!"scrolltopreviousline"();
6446 				//informProgramThatUserChangedPosition(position - step());
6447 			});
6448 			rightButton.addEventListener(EventType.triggered, () {
6449 				this.emitCommand!"scrolltonextline"();
6450 				//informProgramThatUserChangedPosition(position + step());
6451 			});
6452 
6453 			thumb.thumbWidth = this.minWidth;
6454 			thumb.thumbHeight = scaleWithDpi(16);
6455 
6456 			thumb.addEventListener(EventType.change, () {
6457 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
6458 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
6459 
6460 				//informProgramThatUserChangedPosition(sx);
6461 
6462 				auto ev = new ScrollToPositionEvent(this, sx);
6463 				ev.dispatch();
6464 			});
6465 		}
6466 	}
6467 
6468 	override int minHeight() { return scaleWithDpi(16); }
6469 	override int maxHeight() { return scaleWithDpi(16); }
6470 	override int minWidth() { return scaleWithDpi(48); }
6471 }
6472 
6473 class ScrollToPositionEvent : Event {
6474 	enum EventString = "scrolltoposition";
6475 
6476 	this(Widget target, int value) {
6477 		this.value = value;
6478 		super(EventString, target);
6479 	}
6480 
6481 	immutable int value;
6482 
6483 	override @property int intValue() {
6484 		return value;
6485 	}
6486 }
6487 
6488 //version(custom_widgets)
6489 //private
6490 class VerticalScrollbar : ScrollbarBase {
6491 
6492 	version(custom_widgets) {
6493 		override int getBarDim() {
6494 			return thumb.height;
6495 		}
6496 
6497 		private MouseTrackingWidget thumb;
6498 	}
6499 
6500 	override void setViewableArea(int a) {
6501 		super.setViewableArea(a);
6502 
6503 		version(win32_widgets) {
6504 			SCROLLINFO info;
6505 			info.cbSize = info.sizeof;
6506 			info.nPage = a + 1;
6507 			info.fMask = SIF_PAGE;
6508 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6509 		} else version(custom_widgets) {
6510 			thumb.positionY = thumbPosition;
6511 			thumb.thumbHeight = thumbSize;
6512 			thumb.redraw();
6513 		} else static assert(0);
6514 
6515 	}
6516 
6517 	override void setMax(int a) {
6518 		super.setMax(a);
6519 		version(win32_widgets) {
6520 			SCROLLINFO info;
6521 			info.cbSize = info.sizeof;
6522 			info.nMin = 0;
6523 			info.nMax = max;
6524 			info.fMask = SIF_RANGE;
6525 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6526 		} else version(custom_widgets) {
6527 			thumb.positionY = thumbPosition;
6528 			thumb.thumbHeight = thumbSize;
6529 			thumb.redraw();
6530 		}
6531 	}
6532 
6533 	override void setPosition(int a) {
6534 		super.setPosition(a);
6535 		version(win32_widgets) {
6536 			SCROLLINFO info;
6537 			info.cbSize = info.sizeof;
6538 			info.fMask = SIF_POS;
6539 			info.nPos = position;
6540 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6541 		} else version(custom_widgets) {
6542 			thumb.positionY = thumbPosition;
6543 			thumb.thumbHeight = thumbSize;
6544 			thumb.redraw();
6545 		} else static assert(0);
6546 	}
6547 
6548 	this(Widget parent) {
6549 		super(parent);
6550 
6551 		version(win32_widgets) {
6552 			createWin32Window(this, "Scrollbar"w, "",
6553 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
6554 		} else version(custom_widgets) {
6555 			auto vl = new VerticalLayout(this);
6556 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
6557 			upButton.setClickRepeat(scrollClickRepeatInterval);
6558 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
6559 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
6560 			downButton.setClickRepeat(scrollClickRepeatInterval);
6561 
6562 			upButton.addEventListener(EventType.triggered, () {
6563 				this.emitCommand!"scrolltopreviousline"();
6564 				//informProgramThatUserChangedPosition(position - step());
6565 			});
6566 			downButton.addEventListener(EventType.triggered, () {
6567 				this.emitCommand!"scrolltonextline"();
6568 				//informProgramThatUserChangedPosition(position + step());
6569 			});
6570 
6571 			thumb.thumbWidth = this.minWidth;
6572 			thumb.thumbHeight = scaleWithDpi(16);
6573 
6574 			thumb.addEventListener(EventType.change, () {
6575 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
6576 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
6577 
6578 				auto ev = new ScrollToPositionEvent(this, sy);
6579 				ev.dispatch();
6580 
6581 				//informProgramThatUserChangedPosition(sy);
6582 			});
6583 
6584 			upButton.tabStop = false;
6585 			downButton.tabStop = false;
6586 			thumb.tabStop = false;
6587 		}
6588 	}
6589 
6590 	override int minWidth() { return scaleWithDpi(16); }
6591 	override int maxWidth() { return scaleWithDpi(16); }
6592 	override int minHeight() { return scaleWithDpi(48); }
6593 }
6594 
6595 
6596 /++
6597 	EXPERIMENTAL
6598 
6599 	A widget specialized for being a container for other widgets.
6600 
6601 	History:
6602 		Added May 29, 2021. Not stabilized at this time.
6603 +/
6604 class WidgetContainer : Widget {
6605 	this(Widget parent) {
6606 		tabStop = false;
6607 		super(parent);
6608 	}
6609 
6610 	override int maxHeight() {
6611 		if(this.children.length == 1) {
6612 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
6613 		} else {
6614 			return int.max;
6615 		}
6616 	}
6617 
6618 	override int maxWidth() {
6619 		if(this.children.length == 1) {
6620 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
6621 		} else {
6622 			return int.max;
6623 		}
6624 	}
6625 
6626 	/+
6627 
6628 	override int minHeight() {
6629 		int largest = 0;
6630 		int margins = 0;
6631 		int lastMargin = 0;
6632 		foreach(child; children) {
6633 			auto mh = child.minHeight();
6634 			if(mh > largest)
6635 				largest = mh;
6636 			margins += mymax(lastMargin, child.marginTop());
6637 			lastMargin = child.marginBottom();
6638 		}
6639 		return largest + margins;
6640 	}
6641 
6642 	override int maxHeight() {
6643 		int largest = 0;
6644 		int margins = 0;
6645 		int lastMargin = 0;
6646 		foreach(child; children) {
6647 			auto mh = child.maxHeight();
6648 			if(mh == int.max)
6649 				return int.max;
6650 			if(mh > largest)
6651 				largest = mh;
6652 			margins += mymax(lastMargin, child.marginTop());
6653 			lastMargin = child.marginBottom();
6654 		}
6655 		return largest + margins;
6656 	}
6657 
6658 	override int minWidth() {
6659 		int min;
6660 		foreach(child; children) {
6661 			auto cm = child.minWidth;
6662 			if(cm > min)
6663 				min = cm;
6664 		}
6665 		return min + paddingLeft + paddingRight;
6666 	}
6667 
6668 	override int minHeight() {
6669 		int min;
6670 		foreach(child; children) {
6671 			auto cm = child.minHeight;
6672 			if(cm > min)
6673 				min = cm;
6674 		}
6675 		return min + paddingTop + paddingBottom;
6676 	}
6677 
6678 	override int maxHeight() {
6679 		int largest = 0;
6680 		int margins = 0;
6681 		int lastMargin = 0;
6682 		foreach(child; children) {
6683 			auto mh = child.maxHeight();
6684 			if(mh == int.max)
6685 				return int.max;
6686 			if(mh > largest)
6687 				largest = mh;
6688 			margins += mymax(lastMargin, child.marginTop());
6689 			lastMargin = child.marginBottom();
6690 		}
6691 		return largest + margins;
6692 	}
6693 
6694 	override int heightStretchiness() {
6695 		int max;
6696 		foreach(child; children) {
6697 			auto c = child.heightStretchiness;
6698 			if(c > max)
6699 				max = c;
6700 		}
6701 		return max;
6702 	}
6703 
6704 	override int marginTop() {
6705 		if(this.children.length)
6706 			return this.children[0].marginTop;
6707 		return 0;
6708 	}
6709 	+/
6710 }
6711 
6712 ///
6713 abstract class Layout : Widget {
6714 	this(Widget parent) {
6715 		tabStop = false;
6716 		super(parent);
6717 	}
6718 }
6719 
6720 /++
6721 	Makes all children minimum width and height, placing them down
6722 	left to right, top to bottom.
6723 
6724 	Useful if you want to make a list of buttons that automatically
6725 	wrap to a new line when necessary.
6726 +/
6727 class InlineBlockLayout : Layout {
6728 	///
6729 	this(Widget parent) { super(parent); }
6730 
6731 	override void recomputeChildLayout() {
6732 		registerMovement();
6733 
6734 		int x = this.paddingLeft, y = this.paddingTop;
6735 
6736 		int lineHeight;
6737 		int previousMargin = 0;
6738 		int previousMarginBottom = 0;
6739 
6740 		foreach(child; children) {
6741 			if(child.hidden)
6742 				continue;
6743 			if(cast(FixedPosition) child) {
6744 				child.recomputeChildLayout();
6745 				continue;
6746 			}
6747 			child.width = child.flexBasisWidth();
6748 			if(child.width == 0)
6749 				child.width = child.minWidth();
6750 			if(child.width == 0)
6751 				child.width = 32;
6752 
6753 			child.height = child.flexBasisHeight();
6754 			if(child.height == 0)
6755 				child.height = child.minHeight();
6756 			if(child.height == 0)
6757 				child.height = 32;
6758 
6759 			if(x + child.width + paddingRight > this.width) {
6760 				x = this.paddingLeft;
6761 				y += lineHeight;
6762 				lineHeight = 0;
6763 				previousMargin = 0;
6764 				previousMarginBottom = 0;
6765 			}
6766 
6767 			auto margin = child.marginLeft;
6768 			if(previousMargin > margin)
6769 				margin = previousMargin;
6770 
6771 			x += margin;
6772 
6773 			child.x = x;
6774 			child.y = y;
6775 
6776 			int marginTopApplied;
6777 			if(child.marginTop > previousMarginBottom) {
6778 				child.y += child.marginTop;
6779 				marginTopApplied = child.marginTop;
6780 			}
6781 
6782 			x += child.width;
6783 			previousMargin = child.marginRight;
6784 
6785 			if(child.marginBottom > previousMarginBottom)
6786 				previousMarginBottom = child.marginBottom;
6787 
6788 			auto h = child.height + previousMarginBottom + marginTopApplied;
6789 			if(h > lineHeight)
6790 				lineHeight = h;
6791 
6792 			child.recomputeChildLayout();
6793 		}
6794 
6795 	}
6796 
6797 	override int minWidth() {
6798 		int min;
6799 		foreach(child; children) {
6800 			auto cm = child.minWidth;
6801 			if(cm > min)
6802 				min = cm;
6803 		}
6804 		return min + paddingLeft + paddingRight;
6805 	}
6806 
6807 	override int minHeight() {
6808 		int min;
6809 		foreach(child; children) {
6810 			auto cm = child.minHeight;
6811 			if(cm > min)
6812 				min = cm;
6813 		}
6814 		return min + paddingTop + paddingBottom;
6815 	}
6816 }
6817 
6818 /++
6819 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
6820 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
6821 	the [TabWidget] will automatically change pages of child widgets.
6822 
6823 	This allows you to react to it however you see fit rather than having to
6824 	be tied to just the new sets of child widgets.
6825 
6826 	It sends the message in the form of `this.emitCommand!"changetab"();`.
6827 
6828 	History:
6829 		Added December 24, 2021 (dub v10.5)
6830 +/
6831 class TabMessageWidget : Widget {
6832 
6833 	protected void tabIndexClicked(int item) {
6834 		this.emitCommand!"changetab"();
6835 	}
6836 
6837 	/++
6838 		Adds the a new tab to the control with the given title.
6839 
6840 		Returns:
6841 			The index of the newly added tab. You will need to know
6842 			this index to refer to it later and to know which tab to
6843 			change to when you get a changetab message.
6844 	+/
6845 	int addTab(string title, int pos = int.max) {
6846 		version(win32_widgets) {
6847 			TCITEM item;
6848 			item.mask = TCIF_TEXT;
6849 			WCharzBuffer buf = WCharzBuffer(title);
6850 			item.pszText = buf.ptr;
6851 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
6852 		} else version(custom_widgets) {
6853 			if(pos >= tabs.length) {
6854 				tabs ~= title;
6855 				redraw();
6856 				return cast(int) tabs.length - 1;
6857 			} else if(pos <= 0) {
6858 				tabs = title ~ tabs;
6859 				redraw();
6860 				return 0;
6861 			} else {
6862 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
6863 				redraw();
6864 				return pos;
6865 			}
6866 		}
6867 	}
6868 
6869 	override void addChild(Widget child, int pos = int.max) {
6870 		if(container)
6871 			container.addChild(child, pos);
6872 		else
6873 			super.addChild(child, pos);
6874 	}
6875 
6876 	protected Widget makeContainer() {
6877 		return new Widget(this);
6878 	}
6879 
6880 	private Widget container;
6881 
6882 	override void recomputeChildLayout() {
6883 		version(win32_widgets) {
6884 			this.registerMovement();
6885 
6886 			RECT rect;
6887 			GetWindowRect(hwnd, &rect);
6888 
6889 			auto left = rect.left;
6890 			auto top = rect.top;
6891 
6892 			TabCtrl_AdjustRect(hwnd, false, &rect);
6893 			foreach(child; children) {
6894 				if(!child.showing) continue;
6895 				child.x = rect.left - left;
6896 				child.y = rect.top - top;
6897 				child.width = rect.right - rect.left;
6898 				child.height = rect.bottom - rect.top;
6899 				child.recomputeChildLayout();
6900 			}
6901 		} else version(custom_widgets) {
6902 			this.registerMovement();
6903 			foreach(child; children) {
6904 				if(!child.showing) continue;
6905 				child.x = 2;
6906 				child.y = tabBarHeight + 2; // for the border
6907 				child.width = width - 4; // for the border
6908 				child.height = height - tabBarHeight - 2 - 2; // for the border
6909 				child.recomputeChildLayout();
6910 			}
6911 		} else static assert(0);
6912 	}
6913 
6914 	version(custom_widgets)
6915 		string[] tabs;
6916 
6917 	this(Widget parent) {
6918 		super(parent);
6919 
6920 		tabStop = false;
6921 
6922 		version(win32_widgets) {
6923 			createWin32Window(this, WC_TABCONTROL, "", 0);
6924 		} else version(custom_widgets) {
6925 			addEventListener((ClickEvent event) {
6926 				if(event.target !is this)
6927 					return;
6928 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
6929 					auto t = (event.clientX / tabWidth);
6930 					if(t >= 0 && t < tabs.length) {
6931 						currentTab_ = t;
6932 						tabIndexClicked(t);
6933 						redraw();
6934 					}
6935 				}
6936 			});
6937 		} else static assert(0);
6938 
6939 		this.container = makeContainer();
6940 	}
6941 
6942 	override int marginTop() { return 4; }
6943 	override int paddingBottom() { return 4; }
6944 
6945 	override int minHeight() {
6946 		int max = 0;
6947 		foreach(child; children)
6948 			max = mymax(child.minHeight, max);
6949 
6950 
6951 		version(win32_widgets) {
6952 			RECT rect;
6953 			rect.right = this.width;
6954 			rect.bottom = max;
6955 			TabCtrl_AdjustRect(hwnd, true, &rect);
6956 
6957 			max = rect.bottom;
6958 		} else {
6959 			max += defaultLineHeight + 4;
6960 		}
6961 
6962 
6963 		return max;
6964 	}
6965 
6966 	version(win32_widgets)
6967 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
6968 		switch(code) {
6969 			case TCN_SELCHANGE:
6970 				auto sel = TabCtrl_GetCurSel(hwnd);
6971 				tabIndexClicked(sel);
6972 			break;
6973 			default:
6974 		}
6975 		return 0;
6976 	}
6977 
6978 	version(custom_widgets) {
6979 		private int currentTab_;
6980 		private int tabBarHeight() { return defaultLineHeight; }
6981 		int tabWidth() { return scaleWithDpi(80); }
6982 	}
6983 
6984 	version(win32_widgets)
6985 	override void paint(WidgetPainter painter) {}
6986 
6987 	version(custom_widgets)
6988 	override void paint(WidgetPainter painter) {
6989 		auto cs = getComputedStyle();
6990 
6991 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
6992 
6993 		int posX = 0;
6994 		foreach(idx, title; tabs) {
6995 			auto isCurrent = idx == getCurrentTab();
6996 
6997 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
6998 
6999 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
7000 			painter.outlineColor = cs.foregroundColor;
7001 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
7002 
7003 			if(isCurrent) {
7004 				painter.outlineColor = cs.windowBackgroundColor;
7005 				painter.fillColor = Color.transparent;
7006 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
7007 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
7008 
7009 				painter.outlineColor = Color.white;
7010 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
7011 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
7012 				painter.outlineColor = cs.activeTabColor;
7013 				painter.drawPixel(Point(posX, tabBarHeight - 1));
7014 			}
7015 
7016 			posX += tabWidth - 2;
7017 		}
7018 	}
7019 
7020 	///
7021 	@scriptable
7022 	void setCurrentTab(int item) {
7023 		version(win32_widgets)
7024 			TabCtrl_SetCurSel(hwnd, item);
7025 		else version(custom_widgets)
7026 			currentTab_ = item;
7027 		else static assert(0);
7028 
7029 		tabIndexClicked(item);
7030 	}
7031 
7032 	///
7033 	@scriptable
7034 	int getCurrentTab() {
7035 		version(win32_widgets)
7036 			return TabCtrl_GetCurSel(hwnd);
7037 		else version(custom_widgets)
7038 			return currentTab_; // FIXME
7039 		else static assert(0);
7040 	}
7041 
7042 	///
7043 	@scriptable
7044 	void removeTab(int item) {
7045 		if(item && item == getCurrentTab())
7046 			setCurrentTab(item - 1);
7047 
7048 		version(win32_widgets) {
7049 			TabCtrl_DeleteItem(hwnd, item);
7050 		}
7051 
7052 		for(int a = item; a < children.length - 1; a++)
7053 			this._children[a] = this._children[a + 1];
7054 		this._children = this._children[0 .. $-1];
7055 	}
7056 
7057 }
7058 
7059 
7060 /++
7061 	A tab widget is a set of clickable tab buttons followed by a content area.
7062 
7063 
7064 	Tabs can change existing content or can be new pages.
7065 
7066 	When the user picks a different tab, a `change` message is generated.
7067 +/
7068 class TabWidget : TabMessageWidget {
7069 	this(Widget parent) {
7070 		super(parent);
7071 	}
7072 
7073 	override protected Widget makeContainer() {
7074 		return null;
7075 	}
7076 
7077 	override void addChild(Widget child, int pos = int.max) {
7078 		if(auto twp = cast(TabWidgetPage) child) {
7079 			Widget.addChild(child, pos);
7080 			if(pos == int.max)
7081 				pos = cast(int) this.children.length - 1;
7082 
7083 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7084 
7085 			if(pos != getCurrentTab) {
7086 				child.showing = false;
7087 			}
7088 		} else {
7089 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7090 		}
7091 	}
7092 
7093 	// FIXME: add tab icons at some point, Windows supports them
7094 	/++
7095 		Adds a page and its associated tab with the given label to the widget.
7096 
7097 		Returns:
7098 			The added page object, to which you can add other widgets.
7099 	+/
7100 	@scriptable
7101 	TabWidgetPage addPage(string title) {
7102 		return new TabWidgetPage(title, this);
7103 	}
7104 
7105 	/++
7106 		Gets the page at the given tab index, or `null` if the index is bad.
7107 
7108 		History:
7109 			Added December 24, 2021.
7110 	+/
7111 	TabWidgetPage getPage(int index) {
7112 		if(index < this.children.length)
7113 			return null;
7114 		return cast(TabWidgetPage) this.children[index];
7115 	}
7116 
7117 	/++
7118 		While you can still use the addTab from the parent class,
7119 		*strongly* recommend you use [addPage] insteaad.
7120 
7121 		History:
7122 			Added December 24, 2021 to fulful the interface
7123 			requirement that came from adding [TabMessageWidget].
7124 
7125 			You should not use it though since the [addPage] function
7126 			is much easier to use here.
7127 	+/
7128 	override int addTab(string title, int pos = int.max) {
7129 		auto p = addPage(title);
7130 		foreach(idx, child; this.children)
7131 			if(child is p)
7132 				return cast(int) idx;
7133 		return -1;
7134 	}
7135 
7136 	protected override void tabIndexClicked(int item) {
7137 		foreach(idx, child; children) {
7138 			child.showing(false, false); // batch the recalculates for the end
7139 		}
7140 
7141 		foreach(idx, child; children) {
7142 			if(idx == item) {
7143 				child.showing(true, false);
7144 				if(parentWindow) {
7145 					auto f = parentWindow.getFirstFocusable(child);
7146 					if(f)
7147 						f.focus();
7148 				}
7149 				recomputeChildLayout();
7150 			}
7151 		}
7152 
7153 		version(win32_widgets) {
7154 			InvalidateRect(hwnd, null, true);
7155 		} else version(custom_widgets) {
7156 			this.redraw();
7157 		}
7158 	}
7159 
7160 }
7161 
7162 /++
7163 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7164 
7165 	You add [TabWidgetPage]s to it.
7166 +/
7167 class PageWidget : Widget {
7168 	this(Widget parent) {
7169 		super(parent);
7170 	}
7171 
7172 	override int minHeight() {
7173 		int max = 0;
7174 		foreach(child; children)
7175 			max = mymax(child.minHeight, max);
7176 
7177 		return max;
7178 	}
7179 
7180 
7181 	override void addChild(Widget child, int pos = int.max) {
7182 		if(auto twp = cast(TabWidgetPage) child) {
7183 			super.addChild(child, pos);
7184 			if(pos == int.max)
7185 				pos = cast(int) this.children.length - 1;
7186 
7187 			if(pos != getCurrentTab) {
7188 				child.showing = false;
7189 			}
7190 		} else {
7191 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7192 		}
7193 	}
7194 
7195 	override void recomputeChildLayout() {
7196 		this.registerMovement();
7197 		foreach(child; children) {
7198 			child.x = 0;
7199 			child.y = 0;
7200 			child.width = width;
7201 			child.height = height;
7202 			child.recomputeChildLayout();
7203 		}
7204 	}
7205 
7206 	private int currentTab_;
7207 
7208 	///
7209 	@scriptable
7210 	void setCurrentTab(int item) {
7211 		currentTab_ = item;
7212 
7213 		showOnly(item);
7214 	}
7215 
7216 	///
7217 	@scriptable
7218 	int getCurrentTab() {
7219 		return currentTab_;
7220 	}
7221 
7222 	///
7223 	@scriptable
7224 	void removeTab(int item) {
7225 		if(item && item == getCurrentTab())
7226 			setCurrentTab(item - 1);
7227 
7228 		for(int a = item; a < children.length - 1; a++)
7229 			this._children[a] = this._children[a + 1];
7230 		this._children = this._children[0 .. $-1];
7231 	}
7232 
7233 	///
7234 	@scriptable
7235 	TabWidgetPage addPage(string title) {
7236 		return new TabWidgetPage(title, this);
7237 	}
7238 
7239 	private void showOnly(int item) {
7240 		foreach(idx, child; children)
7241 			if(idx == item) {
7242 				child.show();
7243 				child.queueRecomputeChildLayout();
7244 			} else {
7245 				child.hide();
7246 			}
7247 	}
7248 }
7249 
7250 /++
7251 
7252 +/
7253 class TabWidgetPage : Widget {
7254 	string title;
7255 	this(string title, Widget parent) {
7256 		this.title = title;
7257 		this.tabStop = false;
7258 		super(parent);
7259 
7260 		///*
7261 		version(win32_widgets) {
7262 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7263 		}
7264 		//*/
7265 	}
7266 
7267 	override int minHeight() {
7268 		int sum = 0;
7269 		foreach(child; children)
7270 			sum += child.minHeight();
7271 		return sum;
7272 	}
7273 }
7274 
7275 version(none)
7276 /++
7277 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7278 
7279 	I think I need to modify the layout algorithms to support this.
7280 +/
7281 class CollapsableSidebar : Widget {
7282 
7283 }
7284 
7285 /// Stacks the widgets vertically, taking all the available width for each child.
7286 class VerticalLayout : Layout {
7287 	// most of this is intentionally blank - widget's default is vertical layout right now
7288 	///
7289 	this(Widget parent) { super(parent); }
7290 
7291 	/++
7292 		Sets a max width for the layout so you don't have to subclass. The max width
7293 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7294 
7295 		History:
7296 			Added November 29, 2021 (dub v10.5)
7297 	+/
7298 	this(int maxWidth, Widget parent) {
7299 		this.mw = maxWidth;
7300 		super(parent);
7301 	}
7302 
7303 	private int mw = int.max;
7304 
7305 	override int maxWidth() { return scaleWithDpi(mw); }
7306 }
7307 
7308 /// Stacks the widgets horizontally, taking all the available height for each child.
7309 class HorizontalLayout : Layout {
7310 	///
7311 	this(Widget parent) { super(parent); }
7312 
7313 	/++
7314 		Sets a max height for the layout so you don't have to subclass. The max height
7315 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7316 
7317 		History:
7318 			Added November 29, 2021 (dub v10.5)
7319 	+/
7320 	this(int maxHeight, Widget parent) {
7321 		this.mh = maxHeight;
7322 		super(parent);
7323 	}
7324 
7325 	private int mh = 0;
7326 
7327 
7328 
7329 	override void recomputeChildLayout() {
7330 		.recomputeChildLayout!"width"(this);
7331 	}
7332 
7333 	override int minHeight() {
7334 		int largest = 0;
7335 		int margins = 0;
7336 		int lastMargin = 0;
7337 		foreach(child; children) {
7338 			auto mh = child.minHeight();
7339 			if(mh > largest)
7340 				largest = mh;
7341 			margins += mymax(lastMargin, child.marginTop());
7342 			lastMargin = child.marginBottom();
7343 		}
7344 		return largest + margins;
7345 	}
7346 
7347 	override int maxHeight() {
7348 		if(mh != 0)
7349 			return mymax(minHeight, scaleWithDpi(mh));
7350 
7351 		int largest = 0;
7352 		int margins = 0;
7353 		int lastMargin = 0;
7354 		foreach(child; children) {
7355 			auto mh = child.maxHeight();
7356 			if(mh == int.max)
7357 				return int.max;
7358 			if(mh > largest)
7359 				largest = mh;
7360 			margins += mymax(lastMargin, child.marginTop());
7361 			lastMargin = child.marginBottom();
7362 		}
7363 		return largest + margins;
7364 	}
7365 
7366 	override int heightStretchiness() {
7367 		int max;
7368 		foreach(child; children) {
7369 			auto c = child.heightStretchiness;
7370 			if(c > max)
7371 				max = c;
7372 		}
7373 		return max;
7374 	}
7375 
7376 }
7377 
7378 version(win32_widgets)
7379 private
7380 extern(Windows)
7381 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
7382 	Widget* pwin = hwnd in Widget.nativeMapping;
7383 	if(pwin is null)
7384 		return DefWindowProc(hwnd, message, wparam, lparam);
7385 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
7386 	if(win is null)
7387 		return DefWindowProc(hwnd, message, wparam, lparam);
7388 
7389 	switch(message) {
7390 		case WM_SIZE:
7391 			auto width = LOWORD(lparam);
7392 			auto height = HIWORD(lparam);
7393 
7394 			auto hdc = GetDC(hwnd);
7395 			auto hdcBmp = CreateCompatibleDC(hdc);
7396 
7397 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
7398 			if(width > win.bmpWidth || height > win.bmpHeight) {
7399 				auto oldBuffer = win.buffer;
7400 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
7401 
7402 				if(oldBuffer)
7403 					DeleteObject(oldBuffer);
7404 
7405 				win.bmpWidth = width;
7406 				win.bmpHeight = height;
7407 			}
7408 
7409 			// just always erase it upon resizing so minigui can draw over with a clean slate
7410 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
7411 
7412 			auto brush = GetSysColorBrush(COLOR_3DFACE);
7413 			RECT r;
7414 			r.left = 0;
7415 			r.top = 0;
7416 			r.right = width;
7417 			r.bottom = height;
7418 			FillRect(hdcBmp, &r, brush);
7419 
7420 			SelectObject(hdcBmp, oldBmp);
7421 			DeleteDC(hdcBmp);
7422 			ReleaseDC(hwnd, hdc);
7423 		break;
7424 		case WM_PAINT:
7425 			if(win.buffer is null)
7426 				goto default;
7427 
7428 			BITMAP bm;
7429 			PAINTSTRUCT ps;
7430 
7431 			HDC hdc = BeginPaint(hwnd, &ps);
7432 
7433 			HDC hdcMem = CreateCompatibleDC(hdc);
7434 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
7435 
7436 			GetObject(win.buffer, bm.sizeof, &bm);
7437 
7438 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
7439 
7440 			SelectObject(hdcMem, hbmOld);
7441 			DeleteDC(hdcMem);
7442 			EndPaint(hwnd, &ps);
7443 		break;
7444 		default:
7445 			return DefWindowProc(hwnd, message, wparam, lparam);
7446 	}
7447 
7448 	return 0;
7449 }
7450 
7451 private wstring Win32Class(wstring name)() {
7452 	static bool classRegistered;
7453 	if(!classRegistered) {
7454 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
7455 		WNDCLASSEX wc;
7456 		wc.cbSize = wc.sizeof;
7457 		wc.hInstance = hInstance;
7458 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
7459 		wc.lpfnWndProc = &DoubleBufferWndProc;
7460 		wc.lpszClassName = name.ptr;
7461 		if(!RegisterClassExW(&wc))
7462 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
7463 		classRegistered = true;
7464 	}
7465 
7466 		return name;
7467 }
7468 
7469 /+
7470 version(win32_widgets)
7471 extern(Windows)
7472 private
7473 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
7474 	switch(iMessage) {
7475 		case WM_PAINT:
7476 			if(auto te = hWnd in Widget.nativeMapping) {
7477 				try {
7478 					//te.redraw();
7479 					writeln(te, " drawing");
7480 				} catch(Exception) {}
7481 			}
7482 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7483 		default:
7484 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7485 	}
7486 }
7487 +/
7488 
7489 
7490 /++
7491 	A widget specifically designed to hold other widgets.
7492 
7493 	History:
7494 		Added July 1, 2021
7495 +/
7496 class ContainerWidget : Widget {
7497 	this(Widget parent) {
7498 		super(parent);
7499 		this.tabStop = false;
7500 
7501 		version(win32_widgets) {
7502 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
7503 		}
7504 	}
7505 }
7506 
7507 /++
7508 	A widget that takes your widget, puts scroll bars around it, and sends
7509 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
7510 	no effort to automatically scroll or clip its child widgets - it just sends
7511 	the messages.
7512 
7513 
7514 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
7515 	The scroll coordinates are all given in a unit you interpret as you wish. One
7516 	of these units is moved on each press of the arrow buttons and represents the
7517 	smallest amount the user can scroll. The intention is for this to be one line,
7518 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
7519 	in each direction that the user might be interested in.
7520 
7521 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
7522 	This is the amount it jumps when the user pressed page up and page down, or clicks
7523 	in the exposed part of the scroll bar.
7524 
7525 	You should add child content to the ScrollMessageWidget. However, it is important to
7526 	note that the coordinates are always independent of the scroll position! It is YOUR
7527 	responsibility to do any necessary transforms, clipping, etc., while drawing the
7528 	content and interpreting mouse events if they are supposed to change with the scroll.
7529 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
7530 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
7531 	you more control (which can be considerably more efficient and adapted to your actual data)
7532 	at the expense of you also needing to be aware of its reality.
7533 
7534 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
7535 	version 10.3. Maybe this will change in the future.... but for now you must call
7536 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
7537 +/
7538 class ScrollMessageWidget : Widget {
7539 	this(Widget parent) {
7540 		super(parent);
7541 
7542 		container = new Widget(this);
7543 		hsb = new HorizontalScrollbar(this);
7544 		vsb = new VerticalScrollbar(this);
7545 
7546 		hsb.addEventListener("scrolltonextline", {
7547 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
7548 			notify();
7549 		});
7550 		hsb.addEventListener("scrolltopreviousline", {
7551 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
7552 			notify();
7553 		});
7554 		vsb.addEventListener("scrolltonextline", {
7555 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
7556 			notify();
7557 		});
7558 		vsb.addEventListener("scrolltopreviousline", {
7559 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
7560 			notify();
7561 		});
7562 		hsb.addEventListener("scrolltonextpage", {
7563 			hsb.setPosition(hsb.position + hsb.step_);
7564 			notify();
7565 		});
7566 		hsb.addEventListener("scrolltopreviouspage", {
7567 			hsb.setPosition(hsb.position - hsb.step_);
7568 			notify();
7569 		});
7570 		vsb.addEventListener("scrolltonextpage", {
7571 			vsb.setPosition(vsb.position + vsb.step_);
7572 			notify();
7573 		});
7574 		vsb.addEventListener("scrolltopreviouspage", {
7575 			vsb.setPosition(vsb.position - vsb.step_);
7576 			notify();
7577 		});
7578 		hsb.addEventListener("scrolltoposition", (Event event) {
7579 			hsb.setPosition(event.intValue);
7580 			notify();
7581 		});
7582 		vsb.addEventListener("scrolltoposition", (Event event) {
7583 			vsb.setPosition(event.intValue);
7584 			notify();
7585 		});
7586 
7587 
7588 		tabStop = false;
7589 		container.tabStop = false;
7590 		magic = true;
7591 	}
7592 
7593 	private int movementPerButtonClickH_ = 1;
7594 	private int movementPerButtonClickV_ = 1;
7595 	public void movementPerButtonClick(int h, int v) {
7596 		movementPerButtonClickH_ = h;
7597 		movementPerButtonClickV_ = v;
7598 	}
7599 
7600 	/++
7601 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
7602 
7603 
7604 		The defaults for [addDefaultWheelListeners] are:
7605 
7606 			$(LIST
7607 				* Mouse wheel scrolls vertically
7608 				* Alt key + mouse wheel scrolls horiontally
7609 				* Shift + mouse wheel scrolls faster.
7610 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
7611 			)
7612 
7613 		The defaults for [addDefaultKeyboardListeners] are:
7614 
7615 			$(LIST
7616 				* Arrow keys scroll by the given amounts
7617 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
7618 				* Page up and down scroll by the vertical viewable area
7619 				* Home and end scroll to the start and end of the verticle viewable area.
7620 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
7621 			)
7622 
7623 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
7624 
7625 		Params:
7626 			horizontalArrowScrollAmount =
7627 			verticalArrowScrollAmount =
7628 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
7629 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
7630 			shiftMultiplier = multiplies the scroll amount by this when shift is held
7631 	+/
7632 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
7633 		auto _this = this;
7634 
7635 		container.addEventListener((scope KeyDownEvent ke) {
7636 			switch(ke.key) {
7637 				case Key.Left:
7638 					_this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7639 				break;
7640 				case Key.Right:
7641 					_this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7642 				break;
7643 				case Key.Up:
7644 					_this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7645 				break;
7646 				case Key.Down:
7647 					_this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7648 				break;
7649 				case Key.PageUp:
7650 					if(ke.altKey)
7651 						_this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7652 					else
7653 						_this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7654 				break;
7655 				case Key.PageDown:
7656 					if(ke.altKey)
7657 						_this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7658 					else
7659 						_this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7660 				break;
7661 				case Key.Home:
7662 					if(ke.altKey)
7663 						_this.scrollLeft(short.max * 16);
7664 					else
7665 						_this.scrollUp(short.max * 16);
7666 				break;
7667 				case Key.End:
7668 					if(ke.altKey)
7669 						_this.scrollRight(short.max * 16);
7670 					else
7671 						_this.scrollDown(short.max * 16);
7672 				break;
7673 
7674 				default:
7675 					// ignore, not for us.
7676 			}
7677 
7678 		});
7679 	}
7680 
7681 	/// ditto
7682 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
7683 		auto _this = this;
7684 		container.addEventListener((scope ClickEvent ce) {
7685 
7686 			//if(ce.target && ce.target.tabStop)
7687 				//ce.target.focus();
7688 
7689 			// ctrl is reserved for the application
7690 			if(ce.ctrlKey)
7691 				return;
7692 
7693 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
7694 				return;
7695 
7696 			if(shiftMultiplier == 0 && ce.shiftKey)
7697 				return;
7698 
7699 			if(ce.button == MouseButton.wheelDown) {
7700 				if(ce.altKey)
7701 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7702 				else
7703 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7704 			} else if(ce.button == MouseButton.wheelUp) {
7705 				if(ce.altKey)
7706 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7707 				else
7708 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7709 			}
7710 		});
7711 	}
7712 
7713 	/++
7714 		Scrolls the given amount.
7715 
7716 		History:
7717 			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.
7718 	+/
7719 	void scrollUp(int amount = 1) {
7720 		vsb.setPosition(vsb.position - amount);
7721 		notify();
7722 	}
7723 	/// ditto
7724 	void scrollDown(int amount = 1) {
7725 		vsb.setPosition(vsb.position + amount);
7726 		notify();
7727 	}
7728 	/// ditto
7729 	void scrollLeft(int amount = 1) {
7730 		hsb.setPosition(hsb.position - amount);
7731 		notify();
7732 	}
7733 	/// ditto
7734 	void scrollRight(int amount = 1) {
7735 		hsb.setPosition(hsb.position + amount);
7736 		notify();
7737 	}
7738 
7739 	///
7740 	VerticalScrollbar verticalScrollBar() { return vsb; }
7741 	///
7742 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
7743 
7744 	void notify() {
7745 		static bool insideNotify;
7746 
7747 		if(insideNotify)
7748 			return; // avoid the recursive call, even if it isn't strictly correct
7749 
7750 		insideNotify = true;
7751 		scope(exit) insideNotify = false;
7752 
7753 		this.emit!ScrollEvent();
7754 	}
7755 
7756 	mixin Emits!ScrollEvent;
7757 
7758 	///
7759 	Point position() {
7760 		return Point(hsb.position, vsb.position);
7761 	}
7762 
7763 	///
7764 	void setPosition(int x, int y) {
7765 		hsb.setPosition(x);
7766 		vsb.setPosition(y);
7767 	}
7768 
7769 	///
7770 	void setPageSize(int unitsX, int unitsY) {
7771 		hsb.setStep(unitsX);
7772 		vsb.setStep(unitsY);
7773 	}
7774 
7775 	/// Always call this BEFORE setViewableArea
7776 	void setTotalArea(int width, int height) {
7777 		hsb.setMax(width);
7778 		vsb.setMax(height);
7779 	}
7780 
7781 	/++
7782 		Always set the viewable area AFTER setitng the total area if you are going to change both.
7783 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
7784 		If you need to do that, use [queueRecomputeChildLayout].
7785 	+/
7786 	void setViewableArea(int width, int height) {
7787 
7788 		// actually there IS A need to dothis cuz the max might have changed since then
7789 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
7790 			//return; // no need to do what is already done
7791 		hsb.setViewableArea(width);
7792 		vsb.setViewableArea(height);
7793 
7794 		bool needsNotify = false;
7795 
7796 		// FIXME: if at any point the rhs is outside the scrollbar, we need
7797 		// to reset to 0. but it should remember the old position in case the
7798 		// window resizes again, so it can kinda return ot where it was.
7799 		//
7800 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
7801 		if(width >= hsb.max) {
7802 			// there's plenty of room to display it all so we need to reset to zero
7803 			// FIXME: adjust so it matches the note above
7804 			hsb.setPosition(0);
7805 			needsNotify = true;
7806 		}
7807 		if(height >= vsb.max) {
7808 			// there's plenty of room to display it all so we need to reset to zero
7809 			// FIXME: adjust so it matches the note above
7810 			vsb.setPosition(0);
7811 			needsNotify = true;
7812 		}
7813 		if(needsNotify)
7814 			notify();
7815 	}
7816 
7817 	private bool magic;
7818 	override void addChild(Widget w, int position = int.max) {
7819 		if(magic)
7820 			container.addChild(w, position);
7821 		else
7822 			super.addChild(w, position);
7823 	}
7824 
7825 	override void recomputeChildLayout() {
7826 		if(hsb is null || vsb is null || container is null) return;
7827 
7828 		registerMovement();
7829 
7830 		enum BUTTON_SIZE = 16;
7831 
7832 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
7833 		hsb.x = 0;
7834 		hsb.y = this.height - hsb.height;
7835 
7836 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
7837 		vsb.x = this.width - vsb.width;
7838 		vsb.y = 0;
7839 
7840 		auto vsb_width = vsb.showing ? vsb.width : 0;
7841 		auto hsb_height = hsb.showing ? hsb.height : 0;
7842 
7843 		hsb.width = this.width - vsb_width;
7844 		vsb.height = this.height - hsb_height;
7845 
7846 		hsb.recomputeChildLayout();
7847 		vsb.recomputeChildLayout();
7848 
7849 		if(this.header is null) {
7850 			container.x = 0;
7851 			container.y = 0;
7852 			container.width = this.width - vsb_width;
7853 			container.height = this.height - hsb_height;
7854 			container.recomputeChildLayout();
7855 		} else {
7856 			header.x = 0;
7857 			header.y = 0;
7858 			header.width = this.width - vsb_width;
7859 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
7860 			header.recomputeChildLayout();
7861 
7862 			container.x = 0;
7863 			container.y = scaleWithDpi(BUTTON_SIZE);
7864 			container.width = this.width - vsb_width;
7865 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
7866 			container.recomputeChildLayout();
7867 		}
7868 	}
7869 
7870 	private HorizontalScrollbar hsb;
7871 	private VerticalScrollbar vsb;
7872 	Widget container;
7873 	private Widget header;
7874 
7875 	/++
7876 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
7877 
7878 		History:
7879 			Added September 27, 2021 (dub v10.3)
7880 	+/
7881 	Widget getHeader() {
7882 		if(this.header is null) {
7883 			magic = false;
7884 			scope(exit) magic = true;
7885 			this.header = new Widget(this);
7886 			queueRecomputeChildLayout();
7887 		}
7888 		return this.header;
7889 	}
7890 
7891 	/++
7892 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
7893 
7894 		History:
7895 			Added January 3, 2023 (dub v11.0)
7896 	+/
7897 	void scrollIntoView(Rectangle rect) {
7898 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
7899 
7900 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
7901 
7902 		// the lower right is exclusive normally
7903 		auto test = rect.lowerRight;
7904 		if(test.x > 0) test.x--;
7905 		if(test.y > 0) test.y--;
7906 
7907 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
7908 			// try to scroll only one dimension at a time if we can
7909 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
7910 				setPosition(rect.upperLeft.x, position.y);
7911 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
7912 				setPosition(position.x, rect.upperLeft.y);
7913 		}
7914 
7915 	}
7916 
7917 	override int minHeight() {
7918 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
7919 		if(header !is null)
7920 			min += header.minHeight;
7921 		if(horizontalScrollBar.showing)
7922 			min += horizontalScrollBar.minHeight;
7923 		return min;
7924 	}
7925 
7926 	override int maxHeight() {
7927 		int max = container ? container.maxHeight : int.max;
7928 		if(max == int.max)
7929 			return max;
7930 		if(horizontalScrollBar.showing)
7931 			max += horizontalScrollBar.minHeight;
7932 		return max;
7933 	}
7934 }
7935 
7936 /++
7937 	$(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")
7938 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
7939 +/
7940 version(minigui_screenshots)
7941 @Screenshot("ScrollMessageWidget")
7942 unittest {
7943 	auto window = new Window("ScrollMessageWidget");
7944 
7945 	auto smw = new ScrollMessageWidget(window);
7946 	smw.addDefaultKeyboardListeners();
7947 	smw.addDefaultWheelListeners();
7948 
7949 	window.loop();
7950 }
7951 
7952 /++
7953 	Bypasses automatic layout for its children, using manual positioning and sizing only.
7954 	While you need to manually position them, you must ensure they are inside the StaticLayout's
7955 	bounding box to avoid undefined behavior.
7956 
7957 	You should almost never use this.
7958 +/
7959 class StaticLayout : Layout {
7960 	///
7961 	this(Widget parent) { super(parent); }
7962 	override void recomputeChildLayout() {
7963 		registerMovement();
7964 		foreach(child; children)
7965 			child.recomputeChildLayout();
7966 	}
7967 }
7968 
7969 /++
7970 	Bypasses automatic positioning when being laid out. It is your responsibility to make
7971 	room for this widget in the parent layout.
7972 
7973 	Its children are laid out normally, unless there is exactly one, in which case it takes
7974 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
7975 	can do that with `padding`).
7976 +/
7977 class StaticPosition : Layout {
7978 	///
7979 	this(Widget parent) { super(parent); }
7980 
7981 	override void recomputeChildLayout() {
7982 		registerMovement();
7983 		if(this.children.length == 1) {
7984 			auto child = children[0];
7985 			child.x = 0;
7986 			child.y = 0;
7987 			child.width = this.width;
7988 			child.height = this.height;
7989 			child.recomputeChildLayout();
7990 		} else
7991 		foreach(child; children)
7992 			child.recomputeChildLayout();
7993 	}
7994 
7995 	alias width = typeof(super).width;
7996 	alias height = typeof(super).height;
7997 
7998 	@property int width(int w) @nogc pure @safe nothrow {
7999 		return this._width = w;
8000 	}
8001 
8002 	@property int height(int w) @nogc pure @safe nothrow {
8003 		return this._height = w;
8004 	}
8005 
8006 }
8007 
8008 /++
8009 	FixedPosition is like [StaticPosition], but its coordinates
8010 	are always relative to the viewport, meaning they do not scroll with
8011 	the parent content.
8012 +/
8013 class FixedPosition : StaticPosition {
8014 	///
8015 	this(Widget parent) { super(parent); }
8016 }
8017 
8018 version(win32_widgets)
8019 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
8020 	if(true) {
8021 		// cmd == 0 = menu, cmd == 1 = accelerator
8022 		if(auto item = idm in Action.mapping) {
8023 			foreach(handler; (*item).triggered)
8024 				handler();
8025 		/*
8026 			auto event = new Event("triggered", *item);
8027 			event.button = idm;
8028 			event.dispatch();
8029 		*/
8030 			return 0;
8031 		}
8032 	}
8033 	if(handle)
8034 	if(auto widgetp = handle in Widget.nativeMapping) {
8035 		(*widgetp).handleWmCommand(cmd, idm);
8036 		return 0;
8037 	}
8038 	return 1;
8039 }
8040 
8041 
8042 ///
8043 class Window : Widget {
8044 	int mouseCaptureCount = 0;
8045 	Widget mouseCapturedBy;
8046 	void captureMouse(Widget byWhom) {
8047 		assert(mouseCapturedBy is null || byWhom is mouseCapturedBy);
8048 		mouseCaptureCount++;
8049 		mouseCapturedBy = byWhom;
8050 		win.grabInput(false, true, false);
8051 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
8052 	}
8053 	void releaseMouseCapture() {
8054 		mouseCaptureCount--;
8055 		mouseCapturedBy = null;
8056 		win.releaseInputGrab();
8057 	}
8058 
8059 
8060 	/++
8061 
8062 	+/
8063 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8064 		return .messageBox(this, title, message, style, icon);
8065 	}
8066 
8067 	/// ditto
8068 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8069 		return messageBox(null, message, style, icon);
8070 	}
8071 
8072 
8073 	/++
8074 		Sets the window icon which is often seen in title bars and taskbars.
8075 
8076 		History:
8077 			Added April 5, 2022 (dub v10.8)
8078 	+/
8079 	@property void icon(MemoryImage icon) {
8080 		if(win && icon)
8081 			win.icon = icon;
8082 	}
8083 
8084 	// 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
8085 	// this does NOT change the icon on the window! That's what the other overload is for
8086 	static @property .icon icon(GenericIcons i) {
8087 		return .icon(i);
8088 	}
8089 
8090 	///
8091 	@scriptable
8092 	@property bool focused() {
8093 		return win.focused;
8094 	}
8095 
8096 	static class Style : Widget.Style {
8097 		override WidgetBackground background() {
8098 			version(custom_widgets)
8099 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8100 			else version(win32_widgets)
8101 				return WidgetBackground(Color.transparent);
8102 			else static assert(0);
8103 		}
8104 	}
8105 	mixin OverrideStyle!Style;
8106 
8107 	/++
8108 		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.
8109 	+/
8110 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8111 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8112 	}
8113 
8114 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8115 		OperatingSystemFont font;
8116 		if(auto vt = WidgetPainter.visualTheme) {
8117 			font = vt.defaultFontCached(96); // FIXME
8118 		}
8119 
8120 		if(font is null) {
8121 			static int defaultHeightCache;
8122 			if(defaultHeightCache == 0) {
8123 				font = new OperatingSystemFont;
8124 				font.loadDefault;
8125 				defaultHeightCache = font.height();// * 5 / 4;
8126 			}
8127 			return defaultHeightCache;
8128 		}
8129 
8130 		return font.height();// * 5 / 4;
8131 	}
8132 
8133 	Widget focusedWidget;
8134 
8135 	private SimpleWindow win_;
8136 
8137 	@property {
8138 		/++
8139 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8140 
8141 			History:
8142 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8143 		+/
8144 		public SimpleWindow win() {
8145 			return win_;
8146 		}
8147 		///
8148 		protected void win(SimpleWindow w) {
8149 			win_ = w;
8150 		}
8151 	}
8152 
8153 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8154 	this(Widget p) {
8155 		tabStop = false;
8156 		super(p);
8157 	}
8158 
8159 	private void actualRedraw() {
8160 		if(recomputeChildLayoutRequired)
8161 			recomputeChildLayoutEntry();
8162 		if(!showing) return;
8163 
8164 		assert(parentWindow !is null);
8165 
8166 		auto w = drawableWindow;
8167 		if(w is null)
8168 			w = parentWindow.win;
8169 
8170 		if(w.closed())
8171 			return;
8172 
8173 		auto ugh = this.parent;
8174 		int lox, loy;
8175 		while(ugh) {
8176 			lox += ugh.x;
8177 			loy += ugh.y;
8178 			ugh = ugh.parent;
8179 		}
8180 		auto painter = w.draw(true);
8181 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8182 	}
8183 
8184 
8185 	private bool skipNextChar = false;
8186 
8187 	/++
8188 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8189 
8190 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8191 	+/
8192 	this(SimpleWindow win) {
8193 
8194 		static if(UsingSimpledisplayX11) {
8195 			win.discardAdditionalConnectionState = &discardXConnectionState;
8196 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8197 		}
8198 
8199 		tabStop = false;
8200 		super(null);
8201 		this.win = win;
8202 
8203 		win.addEventListener((Widget.RedrawEvent) {
8204 			if(win.eventQueued!RecomputeEvent) {
8205 				// writeln("skipping");
8206 				return; // let the recompute event do the actual redraw
8207 			}
8208 			this.actualRedraw();
8209 		});
8210 
8211 		win.addEventListener((Widget.RecomputeEvent) {
8212 			recomputeChildLayoutEntry();
8213 			if(win.eventQueued!RedrawEvent)
8214 				return; // let the queued one do it
8215 			else {
8216 				// writeln("drawing");
8217 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8218 			}
8219 		});
8220 
8221 		this.width = win.width;
8222 		this.height = win.height;
8223 		this.parentWindow = this;
8224 
8225 		win.closeQuery = () {
8226 			if(this.emit!ClosingEvent())
8227 				win.close();
8228 		};
8229 		win.onClosing = () {
8230 			this.emit!ClosedEvent();
8231 		};
8232 
8233 		win.windowResized = (int w, int h) {
8234 			this.width = w;
8235 			this.height = h;
8236 			queueRecomputeChildLayout();
8237 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8238 			//version(win32_widgets)
8239 				//InvalidateRect(hwnd, null, true);
8240 			redraw();
8241 		};
8242 
8243 		win.onFocusChange = (bool getting) {
8244 			if(this.focusedWidget) {
8245 				if(getting) {
8246 					this.focusedWidget.emit!FocusEvent();
8247 					this.focusedWidget.emit!FocusInEvent();
8248 				} else {
8249 					this.focusedWidget.emit!BlurEvent();
8250 					this.focusedWidget.emit!FocusOutEvent();
8251 				}
8252 			}
8253 
8254 			if(getting) {
8255 				this.emit!FocusEvent();
8256 				this.emit!FocusInEvent();
8257 			} else {
8258 				this.emit!BlurEvent();
8259 				this.emit!FocusOutEvent();
8260 			}
8261 		};
8262 
8263 		win.onDpiChanged = {
8264 			this.queueRecomputeChildLayout();
8265 			auto event = new DpiChangedEvent(this);
8266 			event.sendDirectly();
8267 
8268 			privateDpiChanged();
8269 		};
8270 
8271 		win.setEventHandlers(
8272 			(MouseEvent e) {
8273 				dispatchMouseEvent(e);
8274 			},
8275 			(KeyEvent e) {
8276 				//writefln("%x   %s", cast(uint) e.key, e.key);
8277 				dispatchKeyEvent(e);
8278 			},
8279 			(dchar e) {
8280 				if(e == 13) e = 10; // hack?
8281 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8282 				dispatchCharEvent(e);
8283 			},
8284 		);
8285 
8286 		addEventListener("char", (Widget, Event ev) {
8287 			if(skipNextChar) {
8288 				ev.preventDefault();
8289 				skipNextChar = false;
8290 			}
8291 		});
8292 
8293 		version(win32_widgets)
8294 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
8295 			if(hwnd !is this.win.impl.hwnd)
8296 				return 1; // we don't care... pass it on
8297 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
8298 			if(mustReturn)
8299 				return ret;
8300 			return 1; // pass it on
8301 		};
8302 
8303 		if(Window.newWindowCreated)
8304 			Window.newWindowCreated(this);
8305 	}
8306 
8307 	version(custom_widgets)
8308 	override void defaultEventHandler_click(ClickEvent event) {
8309 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
8310 			if(event.target && event.target.tabStop)
8311 				event.target.focus();
8312 		}
8313 	}
8314 
8315 	private static void delegate(Window) newWindowCreated;
8316 
8317 	version(win32_widgets)
8318 	override void paint(WidgetPainter painter) {
8319 		/*
8320 		RECT rect;
8321 		rect.right = this.width;
8322 		rect.bottom = this.height;
8323 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
8324 		*/
8325 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
8326 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
8327 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
8328 		// since the pen is null, to fill the whole space, we need the +1 on both.
8329 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
8330 		SelectObject(painter.impl.hdc, p);
8331 		SelectObject(painter.impl.hdc, b);
8332 	}
8333 	version(custom_widgets)
8334 	override void paint(WidgetPainter painter) {
8335 		auto cs = getComputedStyle();
8336 		painter.fillColor = cs.windowBackgroundColor;
8337 		painter.outlineColor = cs.windowBackgroundColor;
8338 		painter.drawRectangle(Point(0, 0), this.width, this.height);
8339 	}
8340 
8341 
8342 	override void defaultEventHandler_keydown(KeyDownEvent event) {
8343 		Widget _this = event.target;
8344 
8345 		if(event.key == Key.Tab) {
8346 			/* Window tab ordering is a recursive thingy with each group */
8347 
8348 			// FIXME inefficient
8349 			Widget[] helper(Widget p) {
8350 				if(p.hidden)
8351 					return null;
8352 				Widget[] childOrdering;
8353 
8354 				auto children = p.children.dup;
8355 
8356 				while(true) {
8357 					// UIs should be generally small, so gonna brute force it a little
8358 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
8359 
8360 					Widget smallestTab;
8361 					foreach(ref c; children) {
8362 						if(c is null) continue;
8363 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
8364 							smallestTab = c;
8365 							c = null;
8366 						}
8367 					}
8368 					if(smallestTab !is null) {
8369 						if(smallestTab.tabStop && !smallestTab.hidden)
8370 							childOrdering ~= smallestTab;
8371 						if(!smallestTab.hidden)
8372 							childOrdering ~= helper(smallestTab);
8373 					} else
8374 						break;
8375 
8376 				}
8377 
8378 				return childOrdering;
8379 			}
8380 
8381 			Widget[] tabOrdering = helper(this);
8382 
8383 			Widget recipient;
8384 
8385 			if(tabOrdering.length) {
8386 				bool seenThis = false;
8387 				Widget previous;
8388 				foreach(idx, child; tabOrdering) {
8389 					if(child is focusedWidget) {
8390 
8391 						if(event.shiftKey) {
8392 							if(idx == 0)
8393 								recipient = tabOrdering[$-1];
8394 							else
8395 								recipient = tabOrdering[idx - 1];
8396 							break;
8397 						}
8398 
8399 						seenThis = true;
8400 						if(idx + 1 == tabOrdering.length) {
8401 							// we're at the end, either move to the next group
8402 							// or start back over
8403 							recipient = tabOrdering[0];
8404 						}
8405 						continue;
8406 					}
8407 					if(seenThis) {
8408 						recipient = child;
8409 						break;
8410 					}
8411 					previous = child;
8412 				}
8413 			}
8414 
8415 			if(recipient !is null) {
8416 				//  writeln(typeid(recipient));
8417 				recipient.focus();
8418 
8419 				skipNextChar = true;
8420 			}
8421 		}
8422 
8423 		debug if(event.key == Key.F12) {
8424 			if(devTools) {
8425 				devTools.close();
8426 				devTools = null;
8427 			} else {
8428 				devTools = new DevToolWindow(this);
8429 				devTools.show();
8430 			}
8431 		}
8432 	}
8433 
8434 	debug DevToolWindow devTools;
8435 
8436 
8437 	/++
8438 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
8439 
8440 		History:
8441 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
8442 
8443 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
8444 	+/
8445 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
8446 		if(title is null) {
8447 			import core.runtime;
8448 			if(Runtime.args.length)
8449 				title = Runtime.args[0];
8450 		}
8451 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
8452 
8453 		static if(UsingSimpledisplayX11)
8454 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
8455 		///+
8456 		// for input proxy
8457 		auto display = XDisplayConnection.get;
8458 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
8459 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
8460 		XMapWindow(display, inputProxy);
8461 		// writefln("input proxy: 0x%0x", inputProxy);
8462 		this.inputProxy = new SimpleWindow(inputProxy);
8463 
8464 		XEvent lastEvent;
8465 		this.inputProxy.handleNativeEvent = (XEvent ev) {
8466 			lastEvent = ev;
8467 			return 1;
8468 		};
8469 		this.inputProxy.setEventHandlers(
8470 			(MouseEvent e) {
8471 				dispatchMouseEvent(e);
8472 			},
8473 			(KeyEvent e) {
8474 				//writefln("%x   %s", cast(uint) e.key, e.key);
8475 				if(dispatchKeyEvent(e)) {
8476 					// FIXME: i should trap error
8477 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
8478 						auto thing = nw.focusableWindow();
8479 						if(thing && thing.window) {
8480 							lastEvent.xkey.window = thing.window;
8481 							// writeln("sending event ", lastEvent.xkey);
8482 							trapXErrors( {
8483 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
8484 							});
8485 						}
8486 					}
8487 				}
8488 			},
8489 			(dchar e) {
8490 				if(e == 13) e = 10; // hack?
8491 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8492 				dispatchCharEvent(e);
8493 			},
8494 		);
8495 
8496 		this.inputProxy.populateXic();
8497 		// done
8498 		//+/
8499 		}
8500 
8501 
8502 
8503 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
8504 
8505 		this(win);
8506 	}
8507 
8508 	SimpleWindow inputProxy;
8509 
8510 	private SimpleWindow setRequestedInputFocus() {
8511 		return inputProxy;
8512 	}
8513 
8514 	/// ditto
8515 	this(string title, int width = 500, int height = 500) {
8516 		this(width, height, title);
8517 	}
8518 
8519 	///
8520 	@property string title() { return parentWindow.win.title; }
8521 	///
8522 	@property void title(string title) { parentWindow.win.title = title; }
8523 
8524 	///
8525 	@scriptable
8526 	void close() {
8527 		win.close();
8528 		// I synchronize here upon window closing to ensure all child windows
8529 		// get updated too before the event loop. This avoids some random X errors.
8530 		static if(UsingSimpledisplayX11) {
8531 			runInGuiThread( {
8532 				XSync(XDisplayConnection.get, false);
8533 			});
8534 		}
8535 	}
8536 
8537 	bool dispatchKeyEvent(KeyEvent ev) {
8538 		auto wid = focusedWidget;
8539 		if(wid is null)
8540 			wid = this;
8541 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
8542 		event.originalKeyEvent = ev;
8543 		event.key = ev.key;
8544 		event.state = ev.modifierState;
8545 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8546 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8547 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8548 		event.dispatch();
8549 
8550 		return !event.propagationStopped;
8551 	}
8552 
8553 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
8554 	bool dispatchCharEvent(dchar ch) {
8555 		if(focusedWidget) {
8556 			auto event = new CharEvent(focusedWidget, ch);
8557 			event.dispatch();
8558 			return !event.propagationStopped;
8559 		}
8560 		return true;
8561 	}
8562 
8563 	Widget mouseLastOver;
8564 	Widget mouseLastDownOn;
8565 	bool lastWasDoubleClick;
8566 	bool dispatchMouseEvent(MouseEvent ev) {
8567 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
8568 		auto ele = eleR.widget;
8569 
8570 		auto captureEle = ele;
8571 
8572 		if(mouseCapturedBy !is null) {
8573 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
8574 				captureEle = mouseCapturedBy;
8575 		}
8576 
8577 		// a hack to get it relative to the widget.
8578 		eleR.x = ev.x;
8579 		eleR.y = ev.y;
8580 		auto pain = captureEle;
8581 		while(pain) {
8582 			eleR.x -= pain.x;
8583 			eleR.y -= pain.y;
8584 			pain.addScrollPosition(eleR.x, eleR.y);
8585 			pain = pain.parent;
8586 		}
8587 
8588 		void populateMouseEventBase(MouseEventBase event) {
8589 			event.button = ev.button;
8590 			event.buttonLinear = ev.buttonLinear;
8591 			event.state = ev.modifierState;
8592 			event.clientX = eleR.x;
8593 			event.clientY = eleR.y;
8594 
8595 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8596 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8597 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8598 		}
8599 
8600 		if(ev.type == MouseEventType.buttonPressed) {
8601 			{
8602 				auto event = new MouseDownEvent(captureEle);
8603 				populateMouseEventBase(event);
8604 				event.dispatch();
8605 			}
8606 
8607 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
8608 				auto event = new DoubleClickEvent(captureEle);
8609 				populateMouseEventBase(event);
8610 				event.dispatch();
8611 				lastWasDoubleClick = ev.doubleClick;
8612 			} else {
8613 				lastWasDoubleClick = false;
8614 			}
8615 
8616 			mouseLastDownOn = ele;
8617 		} else if(ev.type == MouseEventType.buttonReleased) {
8618 			{
8619 				auto event = new MouseUpEvent(captureEle);
8620 				populateMouseEventBase(event);
8621 				event.dispatch();
8622 			}
8623 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
8624 				auto event = new ClickEvent(captureEle);
8625 				populateMouseEventBase(event);
8626 				event.dispatch();
8627 			}
8628 		} else if(ev.type == MouseEventType.motion) {
8629 			// motion
8630 			{
8631 				auto event = new MouseMoveEvent(captureEle);
8632 				populateMouseEventBase(event); // fills in button which is meaningless but meh
8633 				event.dispatch();
8634 			}
8635 
8636 			if(mouseLastOver !is ele) {
8637 				if(ele !is null) {
8638 					if(!isAParentOf(ele, mouseLastOver)) {
8639 						ele.setDynamicState(DynamicState.hover, true);
8640 						auto event = new MouseEnterEvent(ele);
8641 						event.relatedTarget = mouseLastOver;
8642 						event.sendDirectly();
8643 
8644 						ele.useStyleProperties((scope Widget.Style s) {
8645 							ele.parentWindow.win.cursor = s.cursor;
8646 						});
8647 					}
8648 				}
8649 
8650 				if(mouseLastOver !is null) {
8651 					if(!isAParentOf(mouseLastOver, ele)) {
8652 						mouseLastOver.setDynamicState(DynamicState.hover, false);
8653 						auto event = new MouseLeaveEvent(mouseLastOver);
8654 						event.relatedTarget = ele;
8655 						event.sendDirectly();
8656 					}
8657 				}
8658 
8659 				if(ele !is null) {
8660 					auto event = new MouseOverEvent(ele);
8661 					event.relatedTarget = mouseLastOver;
8662 					event.dispatch();
8663 				}
8664 
8665 				if(mouseLastOver !is null) {
8666 					auto event = new MouseOutEvent(mouseLastOver);
8667 					event.relatedTarget = ele;
8668 					event.dispatch();
8669 				}
8670 
8671 				mouseLastOver = ele;
8672 			}
8673 		}
8674 
8675 		return true; // FIXME: the event default prevented?
8676 	}
8677 
8678 	/++
8679 		Shows the window and runs the application event loop.
8680 
8681 		Blocks until this window is closed.
8682 
8683 		Bugs:
8684 
8685 		$(PITFALL
8686 			You should always have one event loop live for your application.
8687 			If you make two windows in sequence, the second call to loop (or
8688 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
8689 			might fail:
8690 
8691 			---
8692 			// don't do this!
8693 			auto window = new Window();
8694 			window.loop();
8695 
8696 			// or new Window or new MainWindow, all the same
8697 			auto window2 = new SimpleWindow();
8698 			window2.eventLoop(0); // problematic! might crash
8699 			---
8700 
8701 			simpledisplay's current implementation assumes that final cleanup is
8702 			done when the event loop refcount reaches zero. So after the first
8703 			eventLoop returns, when there isn't already another one active, it assumes
8704 			the program will exit soon and cleans up.
8705 
8706 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
8707 			it eventually, but in the mean time, there's an easy solution:
8708 
8709 			---
8710 			// do this
8711 			EventLoop mainEventLoop = EventLoop.get; // just add this line
8712 
8713 			auto window = new Window();
8714 			window.loop();
8715 
8716 			// or any other type of Window etc.
8717 			auto window2 = new Window();
8718 			window2.loop(); // perfectly fine since mainEventLoop still alive
8719 			---
8720 
8721 			By adding a top-level reference to the event loop, it ensures the final cleanup
8722 			is not performed until it goes out of scope too, letting the individual window loops
8723 			work without trouble despite the bug.
8724 		)
8725 
8726 		History:
8727 			The [BlockingMode] parameter was added on December 8, 2021.
8728 			The default behavior is to block until the application quits
8729 			(so all windows have been closed), unless another minigui or
8730 			simpledisplay event loop is already running, in which case it
8731 			will block until this window closes specifically.
8732 	+/
8733 	@scriptable
8734 	void loop(BlockingMode bm = BlockingMode.automatic) {
8735 		if(win.closed)
8736 			return; // otherwise show will throw
8737 		show();
8738 		win.eventLoopWithBlockingMode(bm, 0);
8739 	}
8740 
8741 	private bool firstShow = true;
8742 
8743 	@scriptable
8744 	override void show() {
8745 		bool rd = false;
8746 		if(firstShow) {
8747 			firstShow = false;
8748 			queueRecomputeChildLayout();
8749 			auto f = getFirstFocusable(this); // FIXME: autofocus?
8750 			if(f)
8751 				f.focus();
8752 			redraw();
8753 		}
8754 		win.show();
8755 		super.show();
8756 	}
8757 	@scriptable
8758 	override void hide() {
8759 		win.hide();
8760 		super.hide();
8761 	}
8762 
8763 	static Widget getFirstFocusable(Widget start) {
8764 		if(start is null)
8765 			return null;
8766 
8767 		foreach(widget; &start.focusableWidgets) {
8768 			return widget;
8769 		}
8770 
8771 		return null;
8772 	}
8773 
8774 	static Widget getLastFocusable(Widget start) {
8775 		if(start is null)
8776 			return null;
8777 
8778 		Widget last;
8779 		foreach(widget; &start.focusableWidgets) {
8780 			last = widget;
8781 		}
8782 
8783 		return last;
8784 	}
8785 
8786 
8787 	mixin Emits!ClosingEvent;
8788 	mixin Emits!ClosedEvent;
8789 }
8790 
8791 /++
8792 	History:
8793 		Added January 12, 2022
8794 +/
8795 class DpiChangedEvent : Event {
8796 	enum EventString = "dpichanged";
8797 
8798 	this(Widget target) {
8799 		super(EventString, target);
8800 	}
8801 }
8802 
8803 debug private class DevToolWindow : Window {
8804 	Window p;
8805 
8806 	TextEdit parentList;
8807 	TextEdit logWindow;
8808 	TextLabel clickX, clickY;
8809 
8810 	this(Window p) {
8811 		this.p = p;
8812 		super(400, 300, "Developer Toolbox");
8813 
8814 		logWindow = new TextEdit(this);
8815 		parentList = new TextEdit(this);
8816 
8817 		auto hl = new HorizontalLayout(this);
8818 		clickX = new TextLabel("", TextAlignment.Right, hl);
8819 		clickY = new TextLabel("", TextAlignment.Right, hl);
8820 
8821 		parentListeners ~= p.addEventListener("*", (Event ev) {
8822 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
8823 		});
8824 
8825 		parentListeners ~= p.addEventListener((ClickEvent ev) {
8826 			auto s = ev.srcElement;
8827 
8828 			string list;
8829 
8830 			void addInfo(Widget s) {
8831 				list ~= s.toString();
8832 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
8833 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
8834 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
8835 				list ~= "\n\theight: " ~ toInternal!string(s.height);
8836 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
8837 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
8838 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
8839 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
8840 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
8841 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
8842 			}
8843 
8844 			addInfo(s);
8845 
8846 			s = s.parent;
8847 			while(s) {
8848 				list ~= "\n";
8849 				addInfo(s);
8850 				s = s.parent;
8851 			}
8852 			parentList.content = list;
8853 
8854 			clickX.label = toInternal!string(ev.clientX);
8855 			clickY.label = toInternal!string(ev.clientY);
8856 		});
8857 	}
8858 
8859 	EventListener[] parentListeners;
8860 
8861 	override void close() {
8862 		assert(p !is null);
8863 		foreach(p; parentListeners)
8864 			p.disconnect();
8865 		parentListeners = null;
8866 		p.devTools = null;
8867 		p = null;
8868 		super.close();
8869 	}
8870 
8871 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
8872 		if(ev.key == Key.F12) {
8873 			this.close();
8874 			if(p)
8875 				p.devTools = null;
8876 		} else {
8877 			super.defaultEventHandler_keydown(ev);
8878 		}
8879 	}
8880 
8881 	void log(T...)(T t) {
8882 		string str;
8883 		import std.conv;
8884 		foreach(i; t)
8885 			str ~= to!string(i);
8886 		str ~= "\n";
8887 		logWindow.addText(str);
8888 
8889 		//version(custom_widgets)
8890 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
8891 	}
8892 }
8893 
8894 /++
8895 	A dialog is a transient window that intends to get information from
8896 	the user before being dismissed.
8897 +/
8898 class Dialog : Window {
8899 	///
8900 	this(Window parent, int width, int height, string title = null) {
8901 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
8902 
8903 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
8904 	}
8905 
8906 	///
8907 	this(Window parent, string title, int width, int height) {
8908 		this(parent, width, height, title);
8909 	}
8910 
8911 	deprecated("Pass an explicit parent window, even if it is `null`")
8912 	this(int width, int height, string title = null) {
8913 		this(null, width, height, title);
8914 	}
8915 
8916 	///
8917 	void OK() {
8918 
8919 	}
8920 
8921 	///
8922 	void Cancel() {
8923 		this.close();
8924 	}
8925 }
8926 
8927 /++
8928 	A custom widget similar to the HTML5 <details> tag.
8929 +/
8930 version(none)
8931 class DetailsView : Widget {
8932 
8933 }
8934 
8935 // FIXME: maybe i should expose the other list views Windows offers too
8936 
8937 /++
8938 	A TableView is a widget made to display a table of data strings.
8939 
8940 
8941 	Future_Directions:
8942 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
8943 
8944 		I will add a selection changed event at some point, as well as item clicked events.
8945 	History:
8946 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
8947 	See_Also:
8948 		[ListWidget] which displays a list of strings without additional columns.
8949 +/
8950 class TableView : Widget {
8951 	/++
8952 
8953 	+/
8954 	this(Widget parent) {
8955 		super(parent);
8956 
8957 		version(win32_widgets) {
8958 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
8959 		} else version(custom_widgets) {
8960 			auto smw = new ScrollMessageWidget(this);
8961 			smw.addDefaultKeyboardListeners();
8962 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
8963 			tvwi = new TableViewWidgetInner(this, smw);
8964 		}
8965 	}
8966 
8967 	// FIXME: auto-size columns on double click of header thing like in Windows
8968 	// it need only make the currently displayed things fit well.
8969 
8970 
8971 	private ColumnInfo[] columns;
8972 	private int itemCount;
8973 
8974 	version(custom_widgets) private {
8975 		TableViewWidgetInner tvwi;
8976 	}
8977 
8978 	/// Passed to [setColumnInfo]
8979 	static struct ColumnInfo {
8980 		const(char)[] name; /// the name displayed in the header
8981 		/++
8982 			The default width, in pixels. As a special case, you can set this to -1
8983 			if you want the system to try to automatically size the width to fit visible
8984 			content. If it can't, it will try to pick a sensible default size.
8985 
8986 			Any other negative value is not allowed and may lead to unpredictable results.
8987 
8988 			History:
8989 				The -1 behavior was specified on December 3, 2021. It actually worked before
8990 				anyway on Win32 but now it is a formal feature with partial Linux support.
8991 
8992 			Bugs:
8993 				It doesn't actually attempt to calculate a best-fit width on Linux as of
8994 				December 3, 2021. I do plan to fix this in the future, but Windows is the
8995 				priority right now. At least it doesn't break things when you use it now.
8996 		+/
8997 		int width;
8998 
8999 		/++
9000 			Alignment of the text in the cell. Applies to the header as well as all data in this
9001 			column.
9002 
9003 			Bugs:
9004 				On Windows, the first column ignores this member and is always left aligned.
9005 				You can work around this by inserting a dummy first column with width = 0
9006 				then putting your actual data in the second column, which does respect the
9007 				alignment.
9008 
9009 				This is a quirk of the operating system's implementation going back a very
9010 				long time and is unlikely to ever be fixed.
9011 		+/
9012 		TextAlignment alignment;
9013 
9014 		/++
9015 			After all the pixel widths have been assigned, any left over
9016 			space is divided up among all columns and distributed to according
9017 			to the widthPercent field.
9018 
9019 
9020 			For example, if you have two fields, both with width 50 and one with
9021 			widthPercent of 25 and the other with widthPercent of 75, and the
9022 			container is 200 pixels wide, first both get their width of 50.
9023 			then the 100 remaining pixels are split up, so the one gets a total
9024 			of 75 pixels and the other gets a total of 125.
9025 
9026 			This is automatically applied as the window is resized.
9027 
9028 			If there is not enough space - that is, when a horizontal scrollbar
9029 			needs to appear - there are 0 pixels divided up, and thus everyone
9030 			gets 0. This can cause a column to shrink out of proportion when
9031 			passing the scroll threshold.
9032 
9033 			It is important to still set a fixed width (that is, to populate the
9034 			`width` field) even if you use the percents because that will be the
9035 			default minimum in the event of a scroll bar appearing.
9036 
9037 			The percents total in the column can never exceed 100 or be less than 0.
9038 			Doing this will trigger an assert error.
9039 
9040 			Implementation note:
9041 
9042 			Please note that percentages are only recalculated 1) upon original
9043 			construction and 2) upon resizing the control. If the user adjusts the
9044 			width of a column, the percentage items will not be updated.
9045 
9046 			On the other hand, if the user adjusts the width of a percentage column
9047 			then resizes the window, it is recalculated, meaning their hand adjustment
9048 			is discarded. This specific behavior may change in the future as it is
9049 			arguably a bug, but I'm not certain yet.
9050 
9051 			History:
9052 				Added November 10, 2021 (dub v10.4)
9053 		+/
9054 		int widthPercent;
9055 
9056 
9057 		private int calculatedWidth;
9058 	}
9059 	/++
9060 		Sets the number of columns along with information about the headers.
9061 
9062 		Please note: on Windows, the first column ignores your alignment preference
9063 		and is always left aligned.
9064 	+/
9065 	void setColumnInfo(ColumnInfo[] columns...) {
9066 
9067 		foreach(ref c; columns) {
9068 			c.name = c.name.idup;
9069 		}
9070 		this.columns = columns.dup;
9071 
9072 		updateCalculatedWidth(false);
9073 
9074 		version(custom_widgets) {
9075 			tvwi.header.updateHeaders();
9076 			tvwi.updateScrolls();
9077 		} else version(win32_widgets)
9078 		foreach(i, column; this.columns) {
9079 			LVCOLUMN lvColumn;
9080 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
9081 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
9082 
9083 			auto bfr = WCharzBuffer(column.name);
9084 			lvColumn.pszText = bfr.ptr;
9085 
9086 			if(column.alignment & TextAlignment.Center)
9087 				lvColumn.fmt = LVCFMT_CENTER;
9088 			else if(column.alignment & TextAlignment.Right)
9089 				lvColumn.fmt = LVCFMT_RIGHT;
9090 			else
9091 				lvColumn.fmt = LVCFMT_LEFT;
9092 
9093 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
9094 				throw new WindowsApiException("Insert Column Fail", GetLastError());
9095 		}
9096 	}
9097 
9098 	private int getActualSetSize(size_t i, bool askWindows) {
9099 		version(win32_widgets)
9100 			if(askWindows)
9101 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
9102 		auto w = columns[i].width;
9103 		if(w == -1)
9104 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9105 		return w;
9106 	}
9107 
9108 	private void updateCalculatedWidth(bool informWindows) {
9109 		int padding;
9110 		version(win32_widgets)
9111 			padding = 4;
9112 		int remaining = this.width;
9113 		foreach(i, column; columns)
9114 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9115 		remaining -= padding;
9116 		if(remaining < 0)
9117 			remaining = 0;
9118 
9119 		int percentTotal;
9120 		foreach(i, ref column; columns) {
9121 			percentTotal += column.widthPercent;
9122 
9123 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9124 
9125 			column.calculatedWidth = c;
9126 
9127 			version(win32_widgets)
9128 			if(informWindows)
9129 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9130 		}
9131 
9132 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9133 		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).");
9134 
9135 
9136 	}
9137 
9138 	override void registerMovement() {
9139 		super.registerMovement();
9140 
9141 		updateCalculatedWidth(true);
9142 	}
9143 
9144 	/++
9145 		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.
9146 	+/
9147 	void setItemCount(int count) {
9148 		this.itemCount = count;
9149 		version(custom_widgets) {
9150 			tvwi.updateScrolls();
9151 			redraw();
9152 		} else version(win32_widgets) {
9153 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9154 		}
9155 	}
9156 
9157 	/++
9158 		Clears all items;
9159 	+/
9160 	void clear() {
9161 		this.itemCount = 0;
9162 		this.columns = null;
9163 		version(custom_widgets) {
9164 			tvwi.header.updateHeaders();
9165 			tvwi.updateScrolls();
9166 			redraw();
9167 		} else version(win32_widgets) {
9168 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9169 		}
9170 	}
9171 
9172 	/+
9173 	version(win32_widgets)
9174 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9175 		auto itemId = dis.itemID;
9176 		auto hdc = dis.hDC;
9177 		auto rect = dis.rcItem;
9178 		switch(dis.itemAction) {
9179 			case ODA_DRAWENTIRE:
9180 
9181 				// FIXME: do other items
9182 				// FIXME: do the focus rectangle i guess
9183 				// FIXME: alignment
9184 				// FIXME: column width
9185 				// FIXME: padding left
9186 				// FIXME: check dpi scaling
9187 				// FIXME: don't owner draw unless it is necessary.
9188 
9189 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9190 				RECT itemRect;
9191 				itemRect.top = 1; // subitem idx, 1-based
9192 				itemRect.left = LVIR_BOUNDS;
9193 
9194 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9195 				itemRect.left += padding;
9196 
9197 				getData(itemId, 0, (in char[] data) {
9198 					auto wdata = WCharzBuffer(data);
9199 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9200 
9201 				});
9202 			goto case;
9203 			case ODA_FOCUS:
9204 				if(dis.itemState & ODS_FOCUS)
9205 					DrawFocusRect(hdc, &rect);
9206 			break;
9207 			case ODA_SELECT:
9208 				// itemState & ODS_SELECTED
9209 			break;
9210 			default:
9211 		}
9212 		return 1;
9213 	}
9214 	+/
9215 
9216 	version(win32_widgets) {
9217 		CellStyle last;
9218 		COLORREF defaultColor;
9219 		COLORREF defaultBackground;
9220 	}
9221 
9222 	version(win32_widgets)
9223 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9224 		switch(code) {
9225 			case NM_CUSTOMDRAW:
9226 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9227 				switch(s.nmcd.dwDrawStage) {
9228 					case CDDS_PREPAINT:
9229 						if(getCellStyle is null)
9230 							return 0;
9231 
9232 						mustReturn = true;
9233 						return CDRF_NOTIFYITEMDRAW;
9234 					case CDDS_ITEMPREPAINT:
9235 						mustReturn = true;
9236 						return CDRF_NOTIFYSUBITEMDRAW;
9237 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9238 						mustReturn = true;
9239 
9240 						if(getCellStyle is null) // this SHOULD never happen...
9241 							return 0;
9242 
9243 						if(s.iSubItem == 0) {
9244 							// Windows resets it per row so we'll use item 0 as a chance
9245 							// to capture these for later
9246 							defaultColor = s.clrText;
9247 							defaultBackground = s.clrTextBk;
9248 						}
9249 
9250 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9251 						// if no special style and no reset needed...
9252 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9253 							return 0; // allow default processing to continue
9254 
9255 						last = style;
9256 
9257 						// might still need to reset or use the preference.
9258 
9259 						if(style.flags & CellStyle.Flags.textColorSet)
9260 							s.clrText = style.textColor.asWindowsColorRef;
9261 						else
9262 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
9263 						if(style.flags & CellStyle.Flags.backgroundColorSet)
9264 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
9265 						else
9266 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
9267 
9268 						return CDRF_NEWFONT;
9269 					default:
9270 						return 0;
9271 
9272 				}
9273 			case NM_RETURN: // no need since i subclass keydown
9274 			break;
9275 			case LVN_COLUMNCLICK:
9276 				auto info = cast(LPNMLISTVIEW) hdr;
9277 				this.emit!HeaderClickedEvent(info.iSubItem);
9278 			break;
9279 			case NM_CLICK:
9280 			case NM_DBLCLK:
9281 			case NM_RCLICK:
9282 			case NM_RDBLCLK:
9283 				// the item/subitem is set here and that can be a useful notification
9284 				// even beyond the normal click notification
9285 			break;
9286 			case LVN_GETDISPINFO:
9287 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
9288 				if(info.item.mask & LVIF_TEXT) {
9289 					if(getData) {
9290 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
9291 							auto bfr = WCharzBuffer(dataReceived);
9292 							auto len = info.item.cchTextMax;
9293 							if(bfr.length < len)
9294 								len = cast(typeof(len)) bfr.length;
9295 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
9296 							info.item.pszText[len] = 0;
9297 						});
9298 					} else {
9299 						info.item.pszText[0] = 0;
9300 					}
9301 					//info.item.iItem
9302 					//if(info.item.iSubItem)
9303 				}
9304 			break;
9305 			default:
9306 		}
9307 		return 0;
9308 	}
9309 
9310 	override bool encapsulatedChildren() {
9311 		return true;
9312 	}
9313 
9314 	/++
9315 		Informs the control that content has changed.
9316 
9317 		History:
9318 			Added November 10, 2021 (dub v10.4)
9319 	+/
9320 	void update() {
9321 		version(custom_widgets)
9322 			redraw();
9323 		else {
9324 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
9325 			UpdateWindow(hwnd);
9326 		}
9327 
9328 
9329 	}
9330 
9331 	/++
9332 		Called by the system to request the text content of an individual cell. You
9333 		should pass the text into the provided `sink` delegate. This function will be
9334 		called for each visible cell as-needed when drawing.
9335 	+/
9336 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
9337 
9338 	/++
9339 		Available per-cell style customization options. Use one of the constructors
9340 		provided to set the values conveniently, or default construct it and set individual
9341 		values yourself. Just remember to set the `flags` so your values are actually used.
9342 		If the flag isn't set, the field is ignored and the system default is used instead.
9343 
9344 		This is returned by the [getCellStyle] delegate.
9345 
9346 		Examples:
9347 			---
9348 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
9349 			auto table = new TableView(window);
9350 			// snip: you would set up columns here
9351 
9352 			// this is how you provide data to the table view class
9353 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
9354 				import std.conv;
9355 				sink(to!string(my_data[row][column]));
9356 			};
9357 
9358 			// and this is how you customize the colors
9359 			table.getCellStyle = delegate(int row, int column) {
9360 				return (my_data[row][column] < 0) ?
9361 					TableView.CellStyle(Color.red); // make negative numbers red
9362 					: TableView.CellStyle.init; // leave the rest alone
9363 			};
9364 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
9365 			---
9366 
9367 		History:
9368 			Added November 27, 2021 (dub v10.4)
9369 	+/
9370 	struct CellStyle {
9371 		/// 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.
9372 		this(Color textColor) {
9373 			this.textColor = textColor;
9374 			this.flags |= Flags.textColorSet;
9375 		}
9376 		/// Sets a custom text and background color.
9377 		this(Color textColor, Color backgroundColor) {
9378 			this.textColor = textColor;
9379 			this.backgroundColor = backgroundColor;
9380 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
9381 		}
9382 
9383 		Color textColor;
9384 		Color backgroundColor;
9385 		int flags; /// bitmask of [Flags]
9386 		/// available options to combine into [flags]
9387 		enum Flags {
9388 			textColorSet = 1 << 0,
9389 			backgroundColorSet = 1 << 1,
9390 		}
9391 	}
9392 	/++
9393 		Companion delegate to [getData] that allows you to custom style each
9394 		cell of the table.
9395 
9396 		Returns:
9397 			A [CellStyle] structure that describes the desired style for the
9398 			given cell. `return CellStyle.init` if you want the default style.
9399 
9400 		History:
9401 			Added November 27, 2021 (dub v10.4)
9402 	+/
9403 	CellStyle delegate(int row, int column) getCellStyle;
9404 
9405 	// i want to be able to do things like draw little colored things to show red for negative numbers
9406 	// or background color indicators or even in-cell charts
9407 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
9408 
9409 	/++
9410 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
9411 	+/
9412 	mixin Emits!HeaderClickedEvent;
9413 }
9414 
9415 /++
9416 	This is emitted by the [TableView] when a user clicks on a column header.
9417 
9418 	Its member `columnIndex` has the zero-based index of the column that was clicked.
9419 
9420 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
9421 
9422 	History:
9423 		Added November 27, 2021 (dub v10.4)
9424 +/
9425 class HeaderClickedEvent : Event {
9426 	enum EventString = "HeaderClicked";
9427 	this(Widget target, int columnIndex) {
9428 		this.columnIndex = columnIndex;
9429 		super(EventString, target);
9430 	}
9431 
9432 	/// The index of the column
9433 	int columnIndex;
9434 
9435 	///
9436 	override @property int intValue() {
9437 		return columnIndex;
9438 	}
9439 }
9440 
9441 version(custom_widgets)
9442 private class TableViewWidgetInner : Widget {
9443 
9444 // wrap this thing in a ScrollMessageWidget
9445 
9446 	TableView tvw;
9447 	ScrollMessageWidget smw;
9448 	HeaderWidget header;
9449 
9450 	this(TableView tvw, ScrollMessageWidget smw) {
9451 		this.tvw = tvw;
9452 		this.smw = smw;
9453 		super(smw);
9454 
9455 		this.tabStop = true;
9456 
9457 		header = new HeaderWidget(this, smw.getHeader());
9458 
9459 		smw.addEventListener("scroll", () {
9460 			this.redraw();
9461 			header.redraw();
9462 		});
9463 
9464 
9465 		// I need headers outside the scroll area but rendered on the same line as the up arrow
9466 		// FIXME: add a fixed header to the SMW
9467 	}
9468 
9469 	enum padding = 3;
9470 
9471 	void updateScrolls() {
9472 		int w;
9473 		foreach(idx, column; tvw.columns) {
9474 			if(column.width == 0) continue;
9475 			w += tvw.getActualSetSize(idx, false);// + padding;
9476 		}
9477 		smw.setTotalArea(w, tvw.itemCount);
9478 		columnsWidth = w;
9479 	}
9480 
9481 	private int columnsWidth;
9482 
9483 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
9484 
9485 	override void registerMovement() {
9486 		super.registerMovement();
9487 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
9488 		smw.setViewableArea(this.width, this.height / lh);
9489 	}
9490 
9491 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
9492 		int x;
9493 		int y;
9494 
9495 		int row = smw.position.y;
9496 
9497 		foreach(lol; 0 .. this.height / lh) {
9498 			if(row >= tvw.itemCount)
9499 				break;
9500 			x = 0;
9501 			foreach(columnNumber, column; tvw.columns) {
9502 				auto x2 = x + column.calculatedWidth;
9503 				auto smwx = smw.position.x;
9504 
9505 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
9506 					auto startX = x;
9507 					auto endX = x + column.calculatedWidth;
9508 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
9509 						case TextAlignment.Left: startX += padding; break;
9510 						case TextAlignment.Center: startX += padding; endX -= padding; break;
9511 						case TextAlignment.Right: endX -= padding; break;
9512 						default: /* broken */ break;
9513 					}
9514 					if(column.width != 0) // no point drawing an invisible column
9515 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
9516 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh)));
9517 
9518 						void dotext(WidgetPainter painter) {
9519 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
9520 						}
9521 
9522 						if(tvw.getCellStyle !is null) {
9523 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
9524 
9525 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
9526 								auto tempPainter = painter;
9527 								tempPainter.fillColor = style.backgroundColor;
9528 								tempPainter.outlineColor = style.backgroundColor;
9529 
9530 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
9531 									Point(endX - smw.position.x, y + lh));
9532 							}
9533 							auto tempPainter = painter;
9534 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
9535 								tempPainter.outlineColor = style.textColor;
9536 
9537 							dotext(tempPainter);
9538 						} else {
9539 							dotext(painter);
9540 						}
9541 					});
9542 				}
9543 
9544 				x += column.calculatedWidth;
9545 			}
9546 			row++;
9547 			y += lh;
9548 		}
9549 		return bounds;
9550 	}
9551 
9552 	static class Style : Widget.Style {
9553 		override WidgetBackground background() {
9554 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
9555 		}
9556 	}
9557 	mixin OverrideStyle!Style;
9558 
9559 	private static class HeaderWidget : Widget {
9560 		/+
9561 			maybe i should do a splitter thing on top of the other widgets
9562 			so the splitter itself isn't really drawn but still replies to mouse events?
9563 		+/
9564 		this(TableViewWidgetInner tvw, Widget parent) {
9565 			super(parent);
9566 			this.tvw = tvw;
9567 
9568 			this.remainder = new Button("", this);
9569 
9570 			this.addEventListener((scope ClickEvent ev) {
9571 				int header = -1;
9572 				foreach(idx, child; this.children[1 .. $]) {
9573 					if(child is ev.target) {
9574 						header = cast(int) idx;
9575 						break;
9576 					}
9577 				}
9578 
9579 				if(header != -1) {
9580 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
9581 					hce.dispatch();
9582 				}
9583 
9584 			});
9585 		}
9586 
9587 		void updateHeaders() {
9588 			foreach(child; children[1 .. $])
9589 				child.removeWidget();
9590 
9591 			foreach(column; tvw.tvw.columns) {
9592 				// the cast is ok because I dup it above, just the type is never changed.
9593 				// all this is private so it should never get messed up.
9594 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
9595 			}
9596 		}
9597 
9598 		Button remainder;
9599 		TableViewWidgetInner tvw;
9600 
9601 		override void recomputeChildLayout() {
9602 			registerMovement();
9603 			int pos;
9604 			foreach(idx, child; children[1 .. $]) {
9605 				if(idx >= tvw.tvw.columns.length)
9606 					continue;
9607 				child.x = pos;
9608 				child.y = 0;
9609 				child.width = tvw.tvw.columns[idx].calculatedWidth;
9610 				child.height = scaleWithDpi(16);// this.height;
9611 				pos += child.width;
9612 
9613 				child.recomputeChildLayout();
9614 			}
9615 
9616 			if(remainder is null)
9617 				return;
9618 
9619 			remainder.x = pos;
9620 			remainder.y = 0;
9621 			if(pos < this.width)
9622 				remainder.width = this.width - pos;// + 4;
9623 			else
9624 				remainder.width = 0;
9625 			remainder.height = scaleWithDpi(16);
9626 
9627 			remainder.recomputeChildLayout();
9628 		}
9629 
9630 		// for the scrollable children mixin
9631 		Point scrollOrigin() {
9632 			return Point(tvw.smw.position.x, 0);
9633 		}
9634 		void paintFrameAndBackground(WidgetPainter painter) { }
9635 
9636 		mixin ScrollableChildren;
9637 	}
9638 }
9639 
9640 /+
9641 
9642 // given struct / array / number / string / etc, make it viewable and editable
9643 class DataViewerWidget : Widget {
9644 
9645 }
9646 +/
9647 
9648 /++
9649 	A line edit box with an associated label.
9650 
9651 	History:
9652 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
9653 
9654 		```
9655 		Old: ________
9656 
9657 		New:
9658 		____________
9659 		```
9660 
9661 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
9662 
9663 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
9664 		horizontal label but left aligned. You may also consider a [GridLayout].
9665 +/
9666 alias LabeledLineEdit = Labeled!LineEdit;
9667 
9668 private int widthThatWouldFitChildLabels(Widget w) {
9669 	if(w is null)
9670 		return 0;
9671 
9672 	int max;
9673 
9674 	if(auto label = cast(TextLabel) w) {
9675 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
9676 	} else {
9677 		foreach(child; w.children) {
9678 			max = mymax(max, widthThatWouldFitChildLabels(child));
9679 		}
9680 	}
9681 
9682 	return max;
9683 }
9684 
9685 /++
9686 	History:
9687 		Added May 19, 2021
9688 +/
9689 class Labeled(T) : Widget {
9690 	///
9691 	this(string label, Widget parent) {
9692 		super(parent);
9693 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
9694 	}
9695 
9696 	/++
9697 		History:
9698 			The alignment parameter was added May 17, 2021
9699 	+/
9700 	this(string label, TextAlignment alignment, Widget parent) {
9701 		super(parent);
9702 		initialize!HorizontalLayout(label, alignment, parent);
9703 	}
9704 
9705 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
9706 		tabStop = false;
9707 		horizontal = is(L == HorizontalLayout);
9708 		auto hl = new L(this);
9709 		if(horizontal) {
9710 			static class SpecialTextLabel : TextLabel {
9711 				Widget outerParent;
9712 
9713 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
9714 					this.outerParent = outerParent;
9715 					super(label, alignment, parent);
9716 				}
9717 
9718 				override int flexBasisWidth() {
9719 					return widthThatWouldFitChildLabels(outerParent);
9720 				}
9721 				/+
9722 				override int widthShrinkiness() { return 0; }
9723 				override int widthStretchiness() { return 1; }
9724 				+/
9725 
9726 				override int paddingRight() { return 6; }
9727 				override int paddingLeft() { return 9; }
9728 
9729 				override int paddingTop() { return 3; }
9730 			}
9731 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
9732 		} else
9733 			this.label = new TextLabel(label, alignment, hl);
9734 		this.lineEdit = new T(hl);
9735 
9736 		this.label.labelFor = this.lineEdit;
9737 	}
9738 
9739 	private bool horizontal;
9740 
9741 	TextLabel label; ///
9742 	T lineEdit; ///
9743 
9744 	override int flexBasisWidth() { return 250; }
9745 	override int widthShrinkiness() { return 1; }
9746 
9747 	override int minHeight() {
9748 		return this.children[0].minHeight;
9749 	}
9750 	override int maxHeight() { return minHeight(); }
9751 	override int marginTop() { return 4; }
9752 	override int marginBottom() { return 4; }
9753 
9754 	// FIXME: i should prolly call it value as well as content tbh
9755 
9756 	///
9757 	@property string content() {
9758 		return lineEdit.content;
9759 	}
9760 	///
9761 	@property void content(string c) {
9762 		return lineEdit.content(c);
9763 	}
9764 
9765 	///
9766 	void selectAll() {
9767 		lineEdit.selectAll();
9768 	}
9769 
9770 	override void focus() {
9771 		lineEdit.focus();
9772 	}
9773 }
9774 
9775 /++
9776 	A labeled password edit.
9777 
9778 	History:
9779 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
9780 
9781 		The default parameters for the constructors were also removed on May 19, 2021
9782 +/
9783 alias LabeledPasswordEdit = Labeled!PasswordEdit;
9784 
9785 private string toMenuLabel(string s) {
9786 	string n;
9787 	n.reserve(s.length);
9788 	foreach(c; s)
9789 		if(c == '_')
9790 			n ~= ' ';
9791 		else
9792 			n ~= c;
9793 	return n;
9794 }
9795 
9796 private void autoExceptionHandler(Exception e) {
9797 	messageBox(e.msg);
9798 }
9799 
9800 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
9801 	static if(is(T : void delegate())) {
9802 		return () {
9803 			try
9804 				t();
9805 			catch(Exception e)
9806 				autoExceptionHandler(e);
9807 		};
9808 	} else static if(is(typeof(fn) Params == __parameters)) {
9809 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
9810 			return () {
9811 				void onOK(string s) {
9812 					member = s;
9813 					try
9814 						t(Params[0](s));
9815 					catch(Exception e)
9816 						autoExceptionHandler(e);
9817 				}
9818 
9819 				if(
9820 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
9821 					|| type == FileDialogType.Save)
9822 				{
9823 					getSaveFileName(window, &onOK, member, filters, null);
9824 				} else
9825 					getOpenFileName(window, &onOK, member, filters, null);
9826 			};
9827 		} else {
9828 			struct S {
9829 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
9830 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
9831 				} else mixin(q{
9832 				static foreach(idx, ignore; Params) {
9833 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
9834 				}
9835 				});
9836 			}
9837 			return () {
9838 				dialog(window, (S s) {
9839 					try {
9840 						static if(is(typeof(t) Ret == return)) {
9841 							static if(is(Ret == void)) {
9842 								t(s.tupleof);
9843 							} else {
9844 								auto ret = t(s.tupleof);
9845 								import std.conv;
9846 								messageBox(to!string(ret), "Returned Value");
9847 							}
9848 						}
9849 					} catch(Exception e)
9850 						autoExceptionHandler(e);
9851 				}, null, __traits(identifier, fn));
9852 			};
9853 		}
9854 	}
9855 }
9856 
9857 private template hasAnyRelevantAnnotations(a...) {
9858 	bool helper() {
9859 		bool any;
9860 		foreach(attr; a) {
9861 			static if(is(typeof(attr) == .menu))
9862 				any = true;
9863 			else static if(is(typeof(attr) == .toolbar))
9864 				any = true;
9865 			else static if(is(attr == .separator))
9866 				any = true;
9867 			else static if(is(typeof(attr) == .accelerator))
9868 				any = true;
9869 			else static if(is(typeof(attr) == .hotkey))
9870 				any = true;
9871 			else static if(is(typeof(attr) == .icon))
9872 				any = true;
9873 			else static if(is(typeof(attr) == .label))
9874 				any = true;
9875 			else static if(is(typeof(attr) == .tip))
9876 				any = true;
9877 		}
9878 		return any;
9879 	}
9880 
9881 	enum bool hasAnyRelevantAnnotations = helper();
9882 }
9883 
9884 /++
9885 	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.
9886 +/
9887 class MainWindow : Window {
9888 	///
9889 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
9890 		super(initialWidth, initialHeight, title);
9891 
9892 		_clientArea = new ClientAreaWidget();
9893 		_clientArea.x = 0;
9894 		_clientArea.y = 0;
9895 		_clientArea.width = this.width;
9896 		_clientArea.height = this.height;
9897 		_clientArea.tabStop = false;
9898 
9899 		super.addChild(_clientArea);
9900 
9901 		statusBar = new StatusBar(this);
9902 	}
9903 
9904 	/++
9905 		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).
9906 
9907 	---
9908         struct Commands {
9909                 @menu("File") {
9910 			@toolbar("") // adds it to a generic toolbar
9911                         void New() {}
9912                         void Open() {}
9913                         void Save() {}
9914                         @separator
9915                         void Exit() @accelerator("Alt+F4") @hotkey('x') {
9916                                 window.close();
9917                         }
9918                 }
9919 
9920                 @menu("Edit") {
9921 			@icon(GenericIcons.Undo)
9922                         void Undo() {
9923                                 undo();
9924                         }
9925                         @separator
9926                         void Cut() {}
9927                         void Copy() {}
9928                         void Paste() {}
9929                 }
9930 
9931                 @menu("Help") {
9932                         void About() {}
9933                 }
9934         }
9935 
9936         Commands commands;
9937 
9938         window.setMenuAndToolbarFromAnnotatedCode(commands);
9939 	---
9940 
9941 	Note that you can call this function multiple times and it will add the items in order to the given items.
9942 
9943 	+/
9944 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
9945 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9946 	}
9947 	/// ditto
9948 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
9949 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9950 	}
9951 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
9952 		Action[] toolbarActions;
9953 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
9954 		Menu[string] mcs;
9955 
9956 		foreach(menu; menuBar.subMenus) {
9957 			mcs[menu.label] = menu;
9958 		}
9959 
9960 		foreach(memberName; __traits(derivedMembers, T)) {
9961 			static if(memberName != "this")
9962 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
9963 				.menu menu;
9964 				.toolbar toolbar;
9965 				bool separator;
9966 				.accelerator accelerator;
9967 				.hotkey hotkey;
9968 				.icon icon;
9969 				string label;
9970 				string tip;
9971 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
9972 					static if(is(typeof(attr) == .menu))
9973 						menu = attr;
9974 					else static if(is(typeof(attr) == .toolbar))
9975 						toolbar = attr;
9976 					else static if(is(attr == .separator))
9977 						separator = true;
9978 					else static if(is(typeof(attr) == .accelerator))
9979 						accelerator = attr;
9980 					else static if(is(typeof(attr) == .hotkey))
9981 						hotkey = attr;
9982 					else static if(is(typeof(attr) == .icon))
9983 						icon = attr;
9984 					else static if(is(typeof(attr) == .label))
9985 						label = attr.label;
9986 					else static if(is(typeof(attr) == .tip))
9987 						tip = attr.tip;
9988 				}
9989 
9990 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
9991 					ushort correctIcon = icon.id; // FIXME
9992 					if(label.length == 0)
9993 						label = memberName.toMenuLabel;
9994 
9995 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
9996 
9997 					auto action = new Action(label, correctIcon, handler);
9998 
9999 					if(accelerator.keyString.length) {
10000 						auto ke = KeyEvent.parse(accelerator.keyString);
10001 						action.accelerator = ke;
10002 						accelerators[ke.toStr] = handler;
10003 					}
10004 
10005 					if(toolbar !is .toolbar.init)
10006 						toolbarActions ~= action;
10007 					if(menu !is .menu.init) {
10008 						Menu mc;
10009 						if(menu.name in mcs) {
10010 							mc = mcs[menu.name];
10011 						} else {
10012 							mc = new Menu(menu.name, this);
10013 							menuBar.addItem(mc);
10014 							mcs[menu.name] = mc;
10015 						}
10016 
10017 						if(separator)
10018 							mc.addSeparator();
10019 						mc.addItem(new MenuItem(action));
10020 					}
10021 				}
10022 			}
10023 		}
10024 
10025 		this.menuBar = menuBar;
10026 
10027 		if(toolbarActions.length) {
10028 			auto tb = new ToolBar(toolbarActions, this);
10029 		}
10030 	}
10031 
10032 	void delegate()[string] accelerators;
10033 
10034 	override void defaultEventHandler_keydown(KeyDownEvent event) {
10035 		auto str = event.originalKeyEvent.toStr;
10036 		if(auto acl = str in accelerators)
10037 			(*acl)();
10038 		super.defaultEventHandler_keydown(event);
10039 	}
10040 
10041 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
10042 		super.defaultEventHandler_mouseover(event);
10043 		if(this.statusBar !is null && event.target.statusTip.length)
10044 			this.statusBar.parts[0].content = event.target.statusTip;
10045 		else if(this.statusBar !is null && this.statusTip.length)
10046 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
10047 	}
10048 
10049 	override void addChild(Widget c, int position = int.max) {
10050 		if(auto tb = cast(ToolBar) c)
10051 			version(win32_widgets)
10052 				super.addChild(c, 0);
10053 			else version(custom_widgets)
10054 				super.addChild(c, menuBar ? 1 : 0);
10055 			else static assert(0);
10056 		else
10057 			clientArea.addChild(c, position);
10058 	}
10059 
10060 	ToolBar _toolBar;
10061 	///
10062 	ToolBar toolBar() { return _toolBar; }
10063 	///
10064 	ToolBar toolBar(ToolBar t) {
10065 		_toolBar = t;
10066 		foreach(child; this.children)
10067 			if(child is t)
10068 				return t;
10069 		version(win32_widgets)
10070 			super.addChild(t, 0);
10071 		else version(custom_widgets)
10072 			super.addChild(t, menuBar ? 1 : 0);
10073 		else static assert(0);
10074 		return t;
10075 	}
10076 
10077 	MenuBar _menu;
10078 	///
10079 	MenuBar menuBar() { return _menu; }
10080 	///
10081 	MenuBar menuBar(MenuBar m) {
10082 		if(m is _menu) {
10083 			version(custom_widgets)
10084 				queueRecomputeChildLayout();
10085 			return m;
10086 		}
10087 
10088 		if(_menu !is null) {
10089 			// make sure it is sanely removed
10090 			// FIXME
10091 		}
10092 
10093 		_menu = m;
10094 
10095 		version(win32_widgets) {
10096 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
10097 		} else version(custom_widgets) {
10098 			super.addChild(m, 0);
10099 
10100 		//	clientArea.y = menu.height;
10101 		//	clientArea.height = this.height - menu.height;
10102 
10103 			queueRecomputeChildLayout();
10104 		} else static assert(false);
10105 
10106 		return _menu;
10107 	}
10108 	private Widget _clientArea;
10109 	///
10110 	@property Widget clientArea() { return _clientArea; }
10111 	protected @property void clientArea(Widget wid) {
10112 		_clientArea = wid;
10113 	}
10114 
10115 	private StatusBar _statusBar;
10116 	/++
10117 		Returns the window's [StatusBar]. Be warned it may be `null`.
10118 	+/
10119 	@property StatusBar statusBar() { return _statusBar; }
10120 	/// ditto
10121 	@property void statusBar(StatusBar bar) {
10122 		if(_statusBar !is null)
10123 			_statusBar.removeWidget();
10124 		_statusBar = bar;
10125 		if(bar !is null)
10126 			super.addChild(_statusBar);
10127 	}
10128 }
10129 
10130 /+
10131 	This is really an implementation detail of [MainWindow]
10132 +/
10133 private class ClientAreaWidget : Widget {
10134 	this() {
10135 		this.tabStop = false;
10136 		super(null);
10137 		//sa = new ScrollableWidget(this);
10138 	}
10139 	/*
10140 	ScrollableWidget sa;
10141 	override void addChild(Widget w, int position) {
10142 		if(sa is null)
10143 			super.addChild(w, position);
10144 		else {
10145 			sa.addChild(w, position);
10146 			sa.setContentSize(this.minWidth + 1, this.minHeight);
10147 			writeln(sa.contentWidth, "x", sa.contentHeight);
10148 		}
10149 	}
10150 	*/
10151 }
10152 
10153 /**
10154 	Toolbars are lists of buttons (typically icons) that appear under the menu.
10155 	Each button ought to correspond to a menu item, represented by [Action] objects.
10156 */
10157 class ToolBar : Widget {
10158 	version(win32_widgets) {
10159 		private int idealHeight;
10160 		override int minHeight() { return idealHeight; }
10161 		override int maxHeight() { return idealHeight; }
10162 	} else version(custom_widgets) {
10163 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
10164 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
10165 	} else static assert(false);
10166 	override int heightStretchiness() { return 0; }
10167 
10168 	version(win32_widgets) {
10169 		HIMAGELIST imageListSmall;
10170 		HIMAGELIST imageListLarge;
10171 	}
10172 
10173 	this(Widget parent) {
10174 		this(null, parent);
10175 	}
10176 
10177 	version(win32_widgets)
10178 	void changeIconSize(bool useLarge) {
10179 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
10180 
10181 		/+
10182 		SIZE size;
10183 		import core.sys.windows.commctrl;
10184 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
10185 		idealHeight = size.cy + 4; // the plus 4 is a hack
10186 		+/
10187 
10188 		idealHeight = useLarge ? 34 : 26;
10189 
10190 		if(parent) {
10191 			parent.queueRecomputeChildLayout();
10192 			parent.redraw();
10193 		}
10194 
10195 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
10196 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
10197 	}
10198 
10199 	///
10200 	this(Action[] actions, Widget parent) {
10201 		super(parent);
10202 
10203 		tabStop = false;
10204 
10205 		version(win32_widgets) {
10206 			// so i like how the flat thing looks on windows, but not on wine
10207 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
10208 			// leave it commented
10209 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
10210 
10211 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
10212 
10213 			imageListSmall = ImageList_Create(
10214 				// width, height
10215 				16, 16,
10216 				ILC_COLOR16 | ILC_MASK,
10217 				16 /*numberOfButtons*/, 0);
10218 
10219 			imageListLarge = ImageList_Create(
10220 				// width, height
10221 				24, 24,
10222 				ILC_COLOR16 | ILC_MASK,
10223 				16 /*numberOfButtons*/, 0);
10224 
10225 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
10226 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
10227 
10228 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
10229 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
10230 
10231 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
10232 
10233 			TBBUTTON[] buttons;
10234 
10235 			// FIXME: I_IMAGENONE is if here is no icon
10236 			foreach(action; actions)
10237 				buttons ~= TBBUTTON(
10238 					MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
10239 					action.id,
10240 					TBSTATE_ENABLED, // state
10241 					0, // style
10242 					0, // reserved array, just zero it out
10243 					0, // dwData
10244 					cast(size_t) toWstringzInternal(action.label) // INT_PTR
10245 				);
10246 
10247 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
10248 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
10249 
10250 			/*
10251 			RECT rect;
10252 			GetWindowRect(hwnd, &rect);
10253 			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
10254 			*/
10255 
10256 			dpiChanged(); // to load the things calling changeIconSize the first time
10257 
10258 			assert(idealHeight);
10259 		} else version(custom_widgets) {
10260 			foreach(action; actions)
10261 				new ToolButton(action, this);
10262 		} else static assert(false);
10263 	}
10264 
10265 	override void recomputeChildLayout() {
10266 		.recomputeChildLayout!"width"(this);
10267 	}
10268 
10269 
10270 	version(win32_widgets)
10271 	override protected void dpiChanged() {
10272 		auto sz = scaleWithDpi(16);
10273 		if(sz >= 20)
10274 			changeIconSize(true);
10275 		else
10276 			changeIconSize(false);
10277 	}
10278 }
10279 
10280 enum toolbarIconSize = 24;
10281 
10282 /// 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.
10283 class ToolButton : Button {
10284 	///
10285 	this(string label, Widget parent) {
10286 		super(label, parent);
10287 		tabStop = false;
10288 	}
10289 	///
10290 	this(Action action, Widget parent) {
10291 		super(action.label, parent);
10292 		tabStop = false;
10293 		this.action = action;
10294 	}
10295 
10296 	version(custom_widgets)
10297 	override void defaultEventHandler_click(ClickEvent event) {
10298 		foreach(handler; action.triggered)
10299 			handler();
10300 	}
10301 
10302 	Action action;
10303 
10304 	override int maxWidth() { return toolbarIconSize; }
10305 	override int minWidth() { return toolbarIconSize; }
10306 	override int maxHeight() { return toolbarIconSize; }
10307 	override int minHeight() { return toolbarIconSize; }
10308 
10309 	version(custom_widgets)
10310 	override void paint(WidgetPainter painter) {
10311 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
10312 		painter.outlineColor = Color.black;
10313 
10314 		// I want to get from 16 to 24. that's * 3 / 2
10315 		static assert(toolbarIconSize >= 16);
10316 		enum multiplier = toolbarIconSize / 8;
10317 		enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0);
10318 		switch(action.iconId) {
10319 			case GenericIcons.New:
10320 				painter.fillColor = Color.white;
10321 				painter.drawPolygon(
10322 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor,
10323 					Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor,
10324 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor
10325 				);
10326 			break;
10327 			case GenericIcons.Save:
10328 				painter.fillColor = Color.white;
10329 				painter.outlineColor = Color.black;
10330 				painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10331 
10332 				// the label
10333 				painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor);
10334 
10335 				// the slider
10336 				painter.fillColor = Color.black;
10337 				painter.outlineColor = Color.black;
10338 				painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor);
10339 
10340 				painter.fillColor = Color.white;
10341 				painter.outlineColor = Color.white;
10342 				// the disc window
10343 				painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor);
10344 			break;
10345 			case GenericIcons.Open:
10346 				painter.fillColor = Color.white;
10347 				painter.drawPolygon(
10348 					Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor,
10349 					Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor);
10350 				painter.drawPolygon(
10351 					Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor,
10352 					Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor,
10353 					Point(2, 6) * multiplier / divisor);
10354 				//painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor);
10355 			break;
10356 			case GenericIcons.Copy:
10357 				painter.fillColor = Color.white;
10358 				painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor);
10359 				painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor);
10360 			break;
10361 			case GenericIcons.Cut:
10362 				painter.fillColor = Color.transparent;
10363 				painter.outlineColor = getComputedStyle.foregroundColor();
10364 				painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor);
10365 				painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor);
10366 				painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor);
10367 				painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor);
10368 			break;
10369 			case GenericIcons.Paste:
10370 				painter.fillColor = Color.white;
10371 				painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor);
10372 				painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10373 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor);
10374 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor);
10375 				painter.fillColor = Color.black;
10376 				painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor);
10377 			break;
10378 			case GenericIcons.Help:
10379 				painter.outlineColor = getComputedStyle.foregroundColor();
10380 				painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10381 			break;
10382 			case GenericIcons.Undo:
10383 				painter.fillColor = Color.transparent;
10384 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10385 				painter.outlineColor = Color.black;
10386 				painter.fillColor = Color.black;
10387 				painter.drawPolygon(
10388 					Point(4, 4) * multiplier / divisor,
10389 					Point(8, 2) * multiplier / divisor,
10390 					Point(8, 6) * multiplier / divisor,
10391 					Point(4, 4) * multiplier / divisor,
10392 				);
10393 			break;
10394 			case GenericIcons.Redo:
10395 				painter.fillColor = Color.transparent;
10396 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10397 				painter.outlineColor = Color.black;
10398 				painter.fillColor = Color.black;
10399 				painter.drawPolygon(
10400 					Point(10, 4) * multiplier / divisor,
10401 					Point(6, 2) * multiplier / divisor,
10402 					Point(6, 6) * multiplier / divisor,
10403 					Point(10, 4) * multiplier / divisor,
10404 				);
10405 			break;
10406 			default:
10407 				painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10408 		}
10409 		return bounds;
10410 		});
10411 	}
10412 
10413 }
10414 
10415 
10416 /++
10417 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
10418 +/
10419 class MenuBar : Widget {
10420 	MenuItem[] items;
10421 	Menu[] subMenus;
10422 
10423 	version(win32_widgets) {
10424 		HMENU handle;
10425 		///
10426 		this(Widget parent = null) {
10427 			super(parent);
10428 
10429 			handle = CreateMenu();
10430 			tabStop = false;
10431 		}
10432 	} else version(custom_widgets) {
10433 		///
10434 		this(Widget parent = null) {
10435 			tabStop = false; // these are selected some other way
10436 			super(parent);
10437 		}
10438 
10439 		mixin Padding!q{2};
10440 	} else static assert(false);
10441 
10442 	version(custom_widgets)
10443 	override void paint(WidgetPainter painter) {
10444 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
10445 	}
10446 
10447 	///
10448 	MenuItem addItem(MenuItem item) {
10449 		this.addChild(item);
10450 		items ~= item;
10451 		version(win32_widgets) {
10452 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
10453 		}
10454 		return item;
10455 	}
10456 
10457 
10458 	///
10459 	Menu addItem(Menu item) {
10460 
10461 		subMenus ~= item;
10462 
10463 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
10464 
10465 		addChild(mbItem);
10466 		items ~= mbItem;
10467 
10468 		version(win32_widgets) {
10469 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
10470 		} else version(custom_widgets) {
10471 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
10472 				item.popup(mbItem);
10473 			};
10474 		} else static assert(false);
10475 
10476 		return item;
10477 	}
10478 
10479 	override void recomputeChildLayout() {
10480 		.recomputeChildLayout!"width"(this);
10481 	}
10482 
10483 	override int maxHeight() { return defaultLineHeight + 4; }
10484 	override int minHeight() { return defaultLineHeight + 4; }
10485 }
10486 
10487 
10488 /**
10489 	Status bars appear at the bottom of a MainWindow.
10490 	They are made out of Parts, with a width and content.
10491 
10492 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
10493 
10494 
10495 	sb.parts[0].content = "Status bar text!";
10496 */
10497 class StatusBar : Widget {
10498 	private Part[] partsArray;
10499 	///
10500 	struct Parts {
10501 		@disable this();
10502 		this(StatusBar owner) { this.owner = owner; }
10503 		//@disable this(this);
10504 		///
10505 		@property int length() { return cast(int) owner.partsArray.length; }
10506 		private StatusBar owner;
10507 		private this(StatusBar owner, Part[] parts) {
10508 			this.owner.partsArray = parts;
10509 			this.owner = owner;
10510 		}
10511 		///
10512 		Part opIndex(int p) {
10513 			if(owner.partsArray.length == 0)
10514 				this ~= new StatusBar.Part(0);
10515 			return owner.partsArray[p];
10516 		}
10517 
10518 		///
10519 		Part opOpAssign(string op : "~" )(Part p) {
10520 			assert(owner.partsArray.length < 255);
10521 			p.owner = this.owner;
10522 			p.idx = cast(int) owner.partsArray.length;
10523 			owner.partsArray ~= p;
10524 
10525 			owner.queueRecomputeChildLayout();
10526 
10527 			version(win32_widgets) {
10528 				int[256] pos;
10529 				int cpos;
10530 				foreach(idx, part; owner.partsArray) {
10531 					if(idx + 1 == owner.partsArray.length)
10532 						pos[idx] = -1;
10533 					else {
10534 						cpos += part.currentlyAssignedWidth;
10535 						pos[idx] = cpos;
10536 					}
10537 				}
10538 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
10539 			} else version(custom_widgets) {
10540 				owner.redraw();
10541 			} else static assert(false);
10542 
10543 			return p;
10544 		}
10545 	}
10546 
10547 	private Parts _parts;
10548 	///
10549 	final @property Parts parts() {
10550 		return _parts;
10551 	}
10552 
10553 	/++
10554 
10555 	+/
10556 	static class Part {
10557 		/++
10558 			History:
10559 				Added September 1, 2023 (dub v11.1)
10560 		+/
10561 		enum WidthUnits {
10562 			/++
10563 				Unscaled pixels as they appear on screen.
10564 
10565 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
10566 			+/
10567 			DeviceDependentPixels,
10568 			/++
10569 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
10570 			+/
10571 			DeviceIndependentPixels,
10572 			/++
10573 				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`).
10574 			+/
10575 			ApproximateCharacters,
10576 			/++
10577 				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.
10578 
10579 				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.
10580 			+/
10581 			Proportional
10582 		}
10583 		private WidthUnits units;
10584 		private int width;
10585 		private StatusBar owner;
10586 
10587 		private int currentlyAssignedWidth;
10588 
10589 		/++
10590 			History:
10591 				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.
10592 
10593 				It now allows you to provide your own value for [WidthUnits].
10594 
10595 				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`.
10596 		+/
10597 		this(int w, WidthUnits units = WidthUnits.Proportional) {
10598 			this.units = units;
10599 			this.width = w;
10600 		}
10601 
10602 		/// ditto
10603 		this(int w = 0) {
10604 			if(w == 0)
10605 				this(w, WidthUnits.Proportional);
10606 			else
10607 				this(w, WidthUnits.DeviceDependentPixels);
10608 		}
10609 
10610 		private int idx;
10611 		private string _content;
10612 		///
10613 		@property string content() { return _content; }
10614 		///
10615 		@property void content(string s) {
10616 			version(win32_widgets) {
10617 				_content = s;
10618 				WCharzBuffer bfr = WCharzBuffer(s);
10619 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
10620 			} else version(custom_widgets) {
10621 				if(_content != s) {
10622 					_content = s;
10623 					owner.redraw();
10624 				}
10625 			} else static assert(false);
10626 		}
10627 	}
10628 	string simpleModeContent;
10629 	bool inSimpleMode;
10630 
10631 
10632 	///
10633 	this(Widget parent) {
10634 		super(null); // FIXME
10635 		_parts = Parts(this);
10636 		tabStop = false;
10637 		version(win32_widgets) {
10638 			parentWindow = parent.parentWindow;
10639 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
10640 
10641 			RECT rect;
10642 			GetWindowRect(hwnd, &rect);
10643 			idealHeight = rect.bottom - rect.top;
10644 			assert(idealHeight);
10645 		} else version(custom_widgets) {
10646 		} else static assert(false);
10647 	}
10648 
10649 	override void recomputeChildLayout() {
10650 		int remainingLength = this.width;
10651 
10652 		int proportionalSum;
10653 		int proportionalCount;
10654 		foreach(idx, part; this.partsArray) {
10655 			with(Part.WidthUnits)
10656 			final switch(part.units) {
10657 				case DeviceDependentPixels:
10658 					part.currentlyAssignedWidth = part.width;
10659 					remainingLength -= part.currentlyAssignedWidth;
10660 				break;
10661 				case DeviceIndependentPixels:
10662 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
10663 					remainingLength -= part.currentlyAssignedWidth;
10664 				break;
10665 				case ApproximateCharacters:
10666 					auto cs = getComputedStyle();
10667 					auto font = cs.font;
10668 
10669 					part.currentlyAssignedWidth = font.averageWidth * this.width;
10670 					remainingLength -= part.currentlyAssignedWidth;
10671 				break;
10672 				case Proportional:
10673 					proportionalSum += part.width;
10674 					proportionalCount ++;
10675 				break;
10676 			}
10677 		}
10678 
10679 		foreach(part; this.partsArray) {
10680 			if(part.units == Part.WidthUnits.Proportional) {
10681 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
10682 				if(proportion == 0)
10683 					proportion = 1;
10684 
10685 				if(proportionalSum == 0)
10686 					proportionalSum = proportionalCount;
10687 
10688 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
10689 			}
10690 		}
10691 
10692 		super.recomputeChildLayout();
10693 	}
10694 
10695 	version(win32_widgets)
10696 	override protected void dpiChanged() {
10697 		RECT rect;
10698 		GetWindowRect(hwnd, &rect);
10699 		idealHeight = rect.bottom - rect.top;
10700 		assert(idealHeight);
10701 	}
10702 
10703 	version(custom_widgets)
10704 	override void paint(WidgetPainter painter) {
10705 		auto cs = getComputedStyle();
10706 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10707 		int cpos = 0;
10708 		foreach(idx, part; this.partsArray) {
10709 			auto partWidth = part.currentlyAssignedWidth;
10710 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
10711 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
10712 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
10713 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
10714 
10715 			painter.outlineColor = cs.foregroundColor();
10716 			painter.fillColor = cs.foregroundColor();
10717 
10718 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
10719 			cpos += partWidth;
10720 		}
10721 	}
10722 
10723 
10724 	version(win32_widgets) {
10725 		private int idealHeight;
10726 		override int maxHeight() { return idealHeight; }
10727 		override int minHeight() { return idealHeight; }
10728 	} else version(custom_widgets) {
10729 		override int maxHeight() { return defaultLineHeight + 4; }
10730 		override int minHeight() { return defaultLineHeight + 4; }
10731 	} else static assert(false);
10732 }
10733 
10734 /// Displays an in-progress indicator without known values
10735 version(none)
10736 class IndefiniteProgressBar : Widget {
10737 	version(win32_widgets)
10738 	this(Widget parent) {
10739 		super(parent);
10740 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
10741 		tabStop = false;
10742 	}
10743 	override int minHeight() { return 10; }
10744 }
10745 
10746 /// A progress bar with a known endpoint and completion amount
10747 class ProgressBar : Widget {
10748 	/++
10749 		History:
10750 			Added March 16, 2022 (dub v10.7)
10751 	+/
10752 	this(int min, int max, Widget parent) {
10753 		this(parent);
10754 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
10755 	}
10756 	this(Widget parent) {
10757 		version(win32_widgets) {
10758 			super(parent);
10759 			createWin32Window(this, "msctls_progress32"w, "", 0);
10760 			tabStop = false;
10761 		} else version(custom_widgets) {
10762 			super(parent);
10763 			max = 100;
10764 			step = 10;
10765 			tabStop = false;
10766 		} else static assert(0);
10767 	}
10768 
10769 	version(custom_widgets)
10770 	override void paint(WidgetPainter painter) {
10771 		auto cs = getComputedStyle();
10772 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10773 		painter.fillColor = cs.progressBarColor;
10774 		painter.drawRectangle(Point(0, 0), width * current / max, height);
10775 	}
10776 
10777 
10778 	version(custom_widgets) {
10779 		int current;
10780 		int max;
10781 		int step;
10782 	}
10783 
10784 	///
10785 	void advanceOneStep() {
10786 		version(win32_widgets)
10787 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
10788 		else version(custom_widgets)
10789 			addToPosition(step);
10790 		else static assert(false);
10791 	}
10792 
10793 	///
10794 	void setStepIncrement(int increment) {
10795 		version(win32_widgets)
10796 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
10797 		else version(custom_widgets)
10798 			step = increment;
10799 		else static assert(false);
10800 	}
10801 
10802 	///
10803 	void addToPosition(int amount) {
10804 		version(win32_widgets)
10805 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
10806 		else version(custom_widgets)
10807 			setPosition(current + amount);
10808 		else static assert(false);
10809 	}
10810 
10811 	///
10812 	void setPosition(int pos) {
10813 		version(win32_widgets)
10814 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
10815 		else version(custom_widgets) {
10816 			current = pos;
10817 			if(current > max)
10818 				current = max;
10819 			redraw();
10820 		}
10821 		else static assert(false);
10822 	}
10823 
10824 	///
10825 	void setRange(ushort min, ushort max) {
10826 		version(win32_widgets)
10827 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
10828 		else version(custom_widgets) {
10829 			this.max = max;
10830 		}
10831 		else static assert(false);
10832 	}
10833 
10834 	override int minHeight() { return 10; }
10835 }
10836 
10837 version(custom_widgets)
10838 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
10839 	thisLabel.reserve(label.length);
10840 	bool justSawAmpersand;
10841 	foreach(ch; label) {
10842 		if(justSawAmpersand) {
10843 			justSawAmpersand = false;
10844 			if(ch == '&') {
10845 				goto plain;
10846 			}
10847 			thisAccelerator = ch;
10848 		} else {
10849 			if(ch == '&') {
10850 				justSawAmpersand = true;
10851 				continue;
10852 			}
10853 			plain:
10854 			thisLabel ~= ch;
10855 		}
10856 	}
10857 }
10858 
10859 /++
10860 	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.
10861 
10862 
10863 	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
10864 
10865 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
10866 
10867 	History:
10868 		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.
10869 +/
10870 class Fieldset : Widget {
10871 	// FIXME: on Windows,it doesn't draw the background on the label
10872 	// on X, it doesn't fix the clipping rectangle for it
10873 	version(win32_widgets)
10874 		override int paddingTop() { return defaultLineHeight; }
10875 	else version(custom_widgets)
10876 		override int paddingTop() { return defaultLineHeight + 2; }
10877 	else static assert(false);
10878 	override int paddingBottom() { return 6; }
10879 	override int paddingLeft() { return 6; }
10880 	override int paddingRight() { return 6; }
10881 
10882 	override int marginLeft() { return 6; }
10883 	override int marginRight() { return 6; }
10884 	override int marginTop() { return 2; }
10885 	override int marginBottom() { return 2; }
10886 
10887 	string legend;
10888 
10889 	version(custom_widgets) private dchar accelerator;
10890 
10891 	this(string legend, Widget parent) {
10892 		version(win32_widgets) {
10893 			super(parent);
10894 			this.legend = legend;
10895 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
10896 			tabStop = false;
10897 		} else version(custom_widgets) {
10898 			super(parent);
10899 			tabStop = false;
10900 
10901 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
10902 		} else static assert(0);
10903 	}
10904 
10905 	version(custom_widgets)
10906 	override void paint(WidgetPainter painter) {
10907 		auto dlh = defaultLineHeight;
10908 
10909 		painter.fillColor = Color.transparent;
10910 		auto cs = getComputedStyle();
10911 		painter.pen = Pen(cs.foregroundColor, 1);
10912 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
10913 
10914 		auto tx = painter.textSize(legend);
10915 		painter.outlineColor = Color.transparent;
10916 
10917 		version(Windows) {
10918 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
10919 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
10920 			SelectObject(painter.impl.hdc, b);
10921 		} else static if(UsingSimpledisplayX11) {
10922 			painter.fillColor = getComputedStyle().windowBackgroundColor;
10923 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
10924 		}
10925 		painter.outlineColor = cs.foregroundColor;
10926 		painter.drawText(Point(8, 0), legend);
10927 	}
10928 
10929 	override int maxHeight() {
10930 		auto m = paddingTop() + paddingBottom();
10931 		foreach(child; children) {
10932 			auto mh = child.maxHeight();
10933 			if(mh == int.max)
10934 				return int.max;
10935 			m += mh;
10936 			m += child.marginBottom();
10937 			m += child.marginTop();
10938 		}
10939 		m += 6;
10940 		if(m < minHeight)
10941 			return minHeight;
10942 		return m;
10943 	}
10944 
10945 	override int minHeight() {
10946 		auto m = paddingTop() + paddingBottom();
10947 		foreach(child; children) {
10948 			m += child.minHeight();
10949 			m += child.marginBottom();
10950 			m += child.marginTop();
10951 		}
10952 		return m + 6;
10953 	}
10954 
10955 	override int minWidth() {
10956 		return 6 + cast(int) this.legend.length * 7;
10957 	}
10958 }
10959 
10960 /++
10961 	$(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")
10962 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
10963 +/
10964 version(minigui_screenshots)
10965 @Screenshot("Fieldset")
10966 unittest {
10967 	auto window = new Window(200, 100);
10968 	auto set = new Fieldset("Baby will", window);
10969 	auto option1 = new Radiobox("Eat", set);
10970 	auto option2 = new Radiobox("Cry", set);
10971 	auto option3 = new Radiobox("Sleep", set);
10972 	window.loop();
10973 }
10974 
10975 /// Draws a line
10976 class HorizontalRule : Widget {
10977 	mixin Margin!q{ 2 };
10978 	override int minHeight() { return 2; }
10979 	override int maxHeight() { return 2; }
10980 
10981 	///
10982 	this(Widget parent) {
10983 		super(parent);
10984 	}
10985 
10986 	override void paint(WidgetPainter painter) {
10987 		auto cs = getComputedStyle();
10988 		painter.outlineColor = cs.darkAccentColor;
10989 		painter.drawLine(Point(0, 0), Point(width, 0));
10990 		painter.outlineColor = cs.lightAccentColor;
10991 		painter.drawLine(Point(0, 1), Point(width, 1));
10992 	}
10993 }
10994 
10995 version(minigui_screenshots)
10996 @Screenshot("HorizontalRule")
10997 /++
10998 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
10999 
11000 +/
11001 unittest {
11002 	auto window = new Window(200, 100);
11003 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
11004 	new HorizontalRule(window);
11005 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
11006 	window.loop();
11007 }
11008 
11009 /// ditto
11010 class VerticalRule : Widget {
11011 	mixin Margin!q{ 2 };
11012 	override int minWidth() { return 2; }
11013 	override int maxWidth() { return 2; }
11014 
11015 	///
11016 	this(Widget parent) {
11017 		super(parent);
11018 	}
11019 
11020 	override void paint(WidgetPainter painter) {
11021 		auto cs = getComputedStyle();
11022 		painter.outlineColor = cs.darkAccentColor;
11023 		painter.drawLine(Point(0, 0), Point(0, height));
11024 		painter.outlineColor = cs.lightAccentColor;
11025 		painter.drawLine(Point(1, 0), Point(1, height));
11026 	}
11027 }
11028 
11029 
11030 ///
11031 class Menu : Window {
11032 	void remove() {
11033 		foreach(i, child; parentWindow.children)
11034 			if(child is this) {
11035 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
11036 				break;
11037 			}
11038 		parentWindow.redraw();
11039 
11040 		parentWindow.releaseMouseCapture();
11041 	}
11042 
11043 	///
11044 	void addSeparator() {
11045 		version(win32_widgets)
11046 			AppendMenu(handle, MF_SEPARATOR, 0, null);
11047 		else version(custom_widgets)
11048 			auto hr = new HorizontalRule(this);
11049 		else static assert(0);
11050 	}
11051 
11052 	override int paddingTop() { return 4; }
11053 	override int paddingBottom() { return 4; }
11054 	override int paddingLeft() { return 2; }
11055 	override int paddingRight() { return 2; }
11056 
11057 	version(win32_widgets) {}
11058 	else version(custom_widgets) {
11059 		SimpleWindow dropDown;
11060 		Widget menuParent;
11061 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
11062 			this.menuParent = parent;
11063 
11064 			int w = 150;
11065 			int h = paddingTop + paddingBottom;
11066 			if(this.children.length) {
11067 				// hacking it to get the ideal height out of recomputeChildLayout
11068 				this.width = w;
11069 				this.height = h;
11070 				this.recomputeChildLayoutEntry();
11071 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
11072 				h += paddingBottom;
11073 
11074 				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
11075 			}
11076 
11077 			if(offsetY == int.min)
11078 				offsetY = parent.defaultLineHeight;
11079 
11080 			auto coord = parent.globalCoordinates();
11081 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
11082 			this.x = 0;
11083 			this.y = 0;
11084 			this.width = dropDown.width;
11085 			this.height = dropDown.height;
11086 			this.drawableWindow = dropDown;
11087 			this.recomputeChildLayoutEntry();
11088 
11089 			static if(UsingSimpledisplayX11)
11090 				XSync(XDisplayConnection.get, 0);
11091 
11092 			dropDown.visibilityChanged = (bool visible) {
11093 				if(visible) {
11094 					this.redraw();
11095 					dropDown.grabInput();
11096 				} else {
11097 					dropDown.releaseInputGrab();
11098 				}
11099 			};
11100 
11101 			dropDown.show();
11102 
11103 			clickListener = this.addEventListener((scope ClickEvent ev) {
11104 				unpopup();
11105 				// need to unlock asap just in case other user handlers block...
11106 				static if(UsingSimpledisplayX11)
11107 					flushGui();
11108 			}, true /* again for asap action */);
11109 		}
11110 
11111 		EventListener clickListener;
11112 	}
11113 	else static assert(false);
11114 
11115 	version(custom_widgets)
11116 	void unpopup() {
11117 		mouseLastOver = mouseLastDownOn = null;
11118 		dropDown.hide();
11119 		if(!menuParent.parentWindow.win.closed) {
11120 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
11121 				maw.setDynamicState(DynamicState.depressed, false);
11122 				maw.setDynamicState(DynamicState.hover, false);
11123 				maw.redraw();
11124 			}
11125 			// menuParent.parentWindow.win.focus();
11126 		}
11127 		clickListener.disconnect();
11128 	}
11129 
11130 	MenuItem[] items;
11131 
11132 	///
11133 	MenuItem addItem(MenuItem item) {
11134 		addChild(item);
11135 		items ~= item;
11136 		version(win32_widgets) {
11137 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11138 		}
11139 		return item;
11140 	}
11141 
11142 	string label;
11143 
11144 	version(win32_widgets) {
11145 		HMENU handle;
11146 		///
11147 		this(string label, Widget parent) {
11148 			// not actually passing the parent since it effs up the drawing
11149 			super(cast(Widget) null);// parent);
11150 			this.label = label;
11151 			handle = CreatePopupMenu();
11152 		}
11153 	} else version(custom_widgets) {
11154 		///
11155 		this(string label, Widget parent) {
11156 
11157 			if(dropDown) {
11158 				dropDown.close();
11159 			}
11160 			dropDown = new SimpleWindow(
11161 				150, 4,
11162 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
11163 
11164 			this.label = label;
11165 
11166 			super(dropDown);
11167 		}
11168 	} else static assert(false);
11169 
11170 	override int maxHeight() { return defaultLineHeight; }
11171 	override int minHeight() { return defaultLineHeight; }
11172 
11173 	version(custom_widgets)
11174 	override void paint(WidgetPainter painter) {
11175 		this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
11176 	}
11177 }
11178 
11179 /++
11180 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
11181 +/
11182 class MenuItem : MouseActivatedWidget {
11183 	Menu submenu;
11184 
11185 	Action action;
11186 	string label;
11187 
11188 	override int paddingLeft() { return 4; }
11189 
11190 	override int maxHeight() { return defaultLineHeight + 4; }
11191 	override int minHeight() { return defaultLineHeight + 4; }
11192 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
11193 	override int maxWidth() {
11194 		if(cast(MenuBar) parent) {
11195 			return minWidth();
11196 		}
11197 		return int.max;
11198 	}
11199 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
11200 	this(string lbl, Widget parent = null) {
11201 		super(parent);
11202 		//label = lbl; // FIXME
11203 		foreach(char ch; lbl) // FIXME
11204 			if(ch != '&') // FIXME
11205 				label ~= ch; // FIXME
11206 		tabStop = false; // these are selected some other way
11207 	}
11208 
11209 	///
11210 	this(Action action, Widget parent = null) {
11211 		assert(action !is null);
11212 		this(action.label, parent);
11213 		this.action = action;
11214 		tabStop = false; // these are selected some other way
11215 	}
11216 
11217 	version(custom_widgets)
11218 	override void paint(WidgetPainter painter) {
11219 		auto cs = getComputedStyle();
11220 		if(dynamicState & DynamicState.depressed)
11221 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11222 		if(dynamicState & DynamicState.hover)
11223 			painter.outlineColor = cs.activeMenuItemColor;
11224 		else
11225 			painter.outlineColor = cs.foregroundColor;
11226 		painter.fillColor = Color.transparent;
11227 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11228 		if(action && action.accelerator !is KeyEvent.init) {
11229 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
11230 
11231 		}
11232 	}
11233 
11234 	static class Style : Widget.Style {
11235 		override bool variesWithState(ulong dynamicStateFlags) {
11236 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11237 		}
11238 	}
11239 	mixin OverrideStyle!Style;
11240 
11241 	override void defaultEventHandler_triggered(Event event) {
11242 		if(action)
11243 		foreach(handler; action.triggered)
11244 			handler();
11245 
11246 		if(auto pmenu = cast(Menu) this.parent)
11247 			pmenu.remove();
11248 
11249 		super.defaultEventHandler_triggered(event);
11250 	}
11251 }
11252 
11253 version(win32_widgets)
11254 /// A "mouse activiated widget" is really just an abstract variant of button.
11255 class MouseActivatedWidget : Widget {
11256 	@property bool isChecked() {
11257 		assert(hwnd);
11258 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
11259 
11260 	}
11261 	@property void isChecked(bool state) {
11262 		assert(hwnd);
11263 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
11264 
11265 	}
11266 
11267 	override void handleWmCommand(ushort cmd, ushort id) {
11268 		if(cmd == 0) {
11269 			auto event = new Event(EventType.triggered, this);
11270 			event.dispatch();
11271 		}
11272 	}
11273 
11274 	this(Widget parent) {
11275 		super(parent);
11276 	}
11277 }
11278 else version(custom_widgets)
11279 /// ditto
11280 class MouseActivatedWidget : Widget {
11281 	@property bool isChecked() { return isChecked_; }
11282 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
11283 
11284 	private bool isChecked_;
11285 
11286 	this(Widget parent) {
11287 		super(parent);
11288 
11289 		addEventListener((MouseDownEvent ev) {
11290 			if(ev.button == MouseButton.left) {
11291 				setDynamicState(DynamicState.depressed, true);
11292 				setDynamicState(DynamicState.hover, true);
11293 				redraw();
11294 			}
11295 		});
11296 
11297 		addEventListener((MouseUpEvent ev) {
11298 			if(ev.button == MouseButton.left) {
11299 				setDynamicState(DynamicState.depressed, false);
11300 				setDynamicState(DynamicState.hover, false);
11301 				redraw();
11302 			}
11303 		});
11304 
11305 		addEventListener((MouseMoveEvent mme) {
11306 			if(!(mme.state & ModifierState.leftButtonDown)) {
11307 				if(dynamicState_ & DynamicState.depressed) {
11308 					setDynamicState(DynamicState.depressed, false);
11309 					redraw();
11310 				}
11311 			}
11312 		});
11313 	}
11314 
11315 	override void defaultEventHandler_focus(Event ev) {
11316 		super.defaultEventHandler_focus(ev);
11317 		this.redraw();
11318 	}
11319 	override void defaultEventHandler_blur(Event ev) {
11320 		super.defaultEventHandler_blur(ev);
11321 		setDynamicState(DynamicState.depressed, false);
11322 		this.redraw();
11323 	}
11324 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
11325 		super.defaultEventHandler_keydown(ev);
11326 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
11327 			setDynamicState(DynamicState.depressed, true);
11328 			setDynamicState(DynamicState.hover, true);
11329 			this.redraw();
11330 		}
11331 	}
11332 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
11333 		super.defaultEventHandler_keyup(ev);
11334 		if(!(dynamicState & DynamicState.depressed))
11335 			return;
11336 		setDynamicState(DynamicState.depressed, false);
11337 		setDynamicState(DynamicState.hover, false);
11338 		this.redraw();
11339 
11340 		auto event = new Event(EventType.triggered, this);
11341 		event.sendDirectly();
11342 	}
11343 	override void defaultEventHandler_click(ClickEvent ev) {
11344 		super.defaultEventHandler_click(ev);
11345 		if(ev.button == MouseButton.left) {
11346 			auto event = new Event(EventType.triggered, this);
11347 			event.sendDirectly();
11348 		}
11349 	}
11350 
11351 }
11352 else static assert(false);
11353 
11354 /*
11355 /++
11356 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
11357 
11358 	Basically the same as a checkbox.
11359 +/
11360 class OnOffSwitch : MouseActivatedWidget {
11361 
11362 }
11363 */
11364 
11365 /++
11366 	History:
11367 		Added June 15, 2021 (dub v10.1)
11368 +/
11369 struct ImageLabel {
11370 	/++
11371 		Defines a label+image combo used by some widgets.
11372 
11373 		If you provide just a text label, that is all the widget will try to
11374 		display. Or just an image will display just that. If you provide both,
11375 		it may display both text and image side by side or display the image
11376 		and offer text on an input event depending on the widget.
11377 
11378 		History:
11379 			The `alignment` parameter was added on September 27, 2021
11380 	+/
11381 	this(string label, TextAlignment alignment = TextAlignment.Center) {
11382 		this.label = label;
11383 		this.displayFlags = DisplayFlags.displayText;
11384 		this.alignment = alignment;
11385 	}
11386 
11387 	/// ditto
11388 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11389 		this.label = label;
11390 		this.image = image;
11391 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11392 		this.alignment = alignment;
11393 	}
11394 
11395 	/// ditto
11396 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11397 		this.image = image;
11398 		this.displayFlags = DisplayFlags.displayImage;
11399 		this.alignment = alignment;
11400 	}
11401 
11402 	/// ditto
11403 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
11404 		this.label = label;
11405 		this.image = image;
11406 		this.alignment = alignment;
11407 		this.displayFlags = displayFlags;
11408 	}
11409 
11410 	string label;
11411 	MemoryImage image;
11412 
11413 	enum DisplayFlags {
11414 		displayText = 1 << 0,
11415 		displayImage = 1 << 1,
11416 	}
11417 
11418 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11419 
11420 	TextAlignment alignment;
11421 }
11422 
11423 /++
11424 	A basic checked or not checked box with an attached label.
11425 
11426 
11427 	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
11428 
11429 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11430 
11431 	History:
11432 		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.
11433 +/
11434 class Checkbox : MouseActivatedWidget {
11435 	version(win32_widgets) {
11436 		override int maxHeight() { return scaleWithDpi(16); }
11437 		override int minHeight() { return scaleWithDpi(16); }
11438 	} else version(custom_widgets) {
11439 		private enum buttonSize = 16;
11440 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11441 		override int minHeight() { return maxHeight(); }
11442 	} else static assert(0);
11443 
11444 	override int marginLeft() { return 4; }
11445 
11446 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
11447 
11448 	/++
11449 		Just an alias because I keep typing checked out of web habit.
11450 
11451 		History:
11452 			Added May 31, 2021
11453 	+/
11454 	alias checked = isChecked;
11455 
11456 	private string label;
11457 	private dchar accelerator;
11458 
11459 	/++
11460 	+/
11461 	this(string label, Widget parent) {
11462 		this(ImageLabel(label), Appearance.checkbox, parent);
11463 	}
11464 
11465 	/// ditto
11466 	this(string label, Appearance appearance, Widget parent) {
11467 		this(ImageLabel(label), appearance, parent);
11468 	}
11469 
11470 	/++
11471 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
11472 
11473 		History:
11474 			Added June 29, 2021 (dub v10.2)
11475 	+/
11476 	enum Appearance {
11477 		checkbox, /// a normal checkbox
11478 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
11479 		//sliderswitch,
11480 	}
11481 	private Appearance appearance;
11482 
11483 	/// ditto
11484 	private this(ImageLabel label, Appearance appearance, Widget parent) {
11485 		super(parent);
11486 		version(win32_widgets) {
11487 			this.label = label.label;
11488 
11489 			uint extraStyle;
11490 			final switch(appearance) {
11491 				case Appearance.checkbox:
11492 				break;
11493 				case Appearance.pushbutton:
11494 					extraStyle |= BS_PUSHLIKE;
11495 				break;
11496 			}
11497 
11498 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
11499 		} else version(custom_widgets) {
11500 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
11501 		} else static assert(0);
11502 	}
11503 
11504 	version(custom_widgets)
11505 	override void paint(WidgetPainter painter) {
11506 		auto cs = getComputedStyle();
11507 		if(isFocused()) {
11508 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11509 			painter.fillColor = cs.windowBackgroundColor;
11510 			painter.drawRectangle(Point(0, 0), width, height);
11511 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11512 		} else {
11513 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
11514 			painter.fillColor = cs.windowBackgroundColor;
11515 			painter.drawRectangle(Point(0, 0), width, height);
11516 		}
11517 
11518 
11519 		painter.outlineColor = Color.black;
11520 		painter.fillColor = Color.white;
11521 		enum rectOffset = 2;
11522 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
11523 
11524 		if(isChecked) {
11525 			auto size = scaleWithDpi(2);
11526 			painter.pen = Pen(Color.black, size);
11527 			// I'm using height so the checkbox is square
11528 			enum padding = 3;
11529 			painter.drawLine(
11530 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
11531 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
11532 			);
11533 			painter.drawLine(
11534 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
11535 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
11536 			);
11537 
11538 			painter.pen = Pen(Color.black, 1);
11539 		}
11540 
11541 		if(label !is null) {
11542 			painter.outlineColor = cs.foregroundColor();
11543 			painter.fillColor = cs.foregroundColor();
11544 
11545 			// i want the centerline of the text to be aligned with the centerline of the checkbox
11546 			/+
11547 			auto font = cs.font();
11548 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
11549 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
11550 			+/
11551 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
11552 		}
11553 	}
11554 
11555 	override void defaultEventHandler_triggered(Event ev) {
11556 		isChecked = !isChecked;
11557 
11558 		this.emit!(ChangeEvent!bool)(&isChecked);
11559 
11560 		redraw();
11561 	}
11562 
11563 	/// Emits a change event with the checked state
11564 	mixin Emits!(ChangeEvent!bool);
11565 }
11566 
11567 /// Adds empty space to a layout.
11568 class VerticalSpacer : Widget {
11569 	///
11570 	this(Widget parent) {
11571 		super(parent);
11572 	}
11573 }
11574 
11575 /// ditto
11576 class HorizontalSpacer : Widget {
11577 	///
11578 	this(Widget parent) {
11579 		super(parent);
11580 		this.tabStop = false;
11581 	}
11582 }
11583 
11584 
11585 /++
11586 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
11587 
11588 
11589 	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
11590 
11591 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11592 
11593 	History:
11594 		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.
11595 +/
11596 class Radiobox : MouseActivatedWidget {
11597 
11598 	version(win32_widgets) {
11599 		override int maxHeight() { return scaleWithDpi(16); }
11600 		override int minHeight() { return scaleWithDpi(16); }
11601 	} else version(custom_widgets) {
11602 		private enum buttonSize = 16;
11603 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11604 		override int minHeight() { return maxHeight(); }
11605 	} else static assert(0);
11606 
11607 	override int marginLeft() { return 4; }
11608 
11609 	// FIXME: make a label getter
11610 	private string label;
11611 	private dchar accelerator;
11612 
11613 	/++
11614 
11615 	+/
11616 	this(string label, Widget parent) {
11617 		super(parent);
11618 		version(win32_widgets) {
11619 			this.label = label;
11620 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
11621 		} else version(custom_widgets) {
11622 			label.extractWindowsStyleLabel(this.label, this.accelerator);
11623 			height = 16;
11624 			width = height + 4 + cast(int) label.length * 16;
11625 		}
11626 	}
11627 
11628 	version(custom_widgets)
11629 	override void paint(WidgetPainter painter) {
11630 		auto cs = getComputedStyle();
11631 
11632 		if(isFocused) {
11633 			painter.fillColor = cs.windowBackgroundColor;
11634 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11635 		} else {
11636 			painter.fillColor = cs.windowBackgroundColor;
11637 			painter.outlineColor = cs.windowBackgroundColor;
11638 		}
11639 		painter.drawRectangle(Point(0, 0), width, height);
11640 
11641 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11642 
11643 		painter.outlineColor = Color.black;
11644 		painter.fillColor = Color.white;
11645 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
11646 		if(isChecked) {
11647 			painter.outlineColor = Color.black;
11648 			painter.fillColor = Color.black;
11649 			// I'm using height so the checkbox is square
11650 			auto size = scaleWithDpi(2);
11651 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
11652 		}
11653 
11654 		painter.outlineColor = cs.foregroundColor();
11655 		painter.fillColor = cs.foregroundColor();
11656 
11657 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11658 	}
11659 
11660 
11661 	override void defaultEventHandler_triggered(Event ev) {
11662 		isChecked = true;
11663 
11664 		if(this.parent) {
11665 			foreach(child; this.parent.children) {
11666 				if(child is this) continue;
11667 				if(auto rb = cast(Radiobox) child) {
11668 					rb.isChecked = false;
11669 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
11670 					rb.redraw();
11671 				}
11672 			}
11673 		}
11674 
11675 		this.emit!(ChangeEvent!bool)(&this.isChecked);
11676 
11677 		redraw();
11678 	}
11679 
11680 	/// 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.
11681 	mixin Emits!(ChangeEvent!bool);
11682 }
11683 
11684 
11685 /++
11686 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
11687 
11688 
11689 	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
11690 
11691 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11692 
11693 	History:
11694 		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.
11695 +/
11696 class Button : MouseActivatedWidget {
11697 	override int heightStretchiness() { return 3; }
11698 	override int widthStretchiness() { return 3; }
11699 
11700 	/++
11701 		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.
11702 
11703 		History:
11704 			Added July 2, 2021
11705 	+/
11706 	public bool triggersOnMultiClick;
11707 
11708 	private string label_;
11709 	private TextAlignment alignment;
11710 	private dchar accelerator;
11711 
11712 	///
11713 	string label() { return label_; }
11714 	///
11715 	void label(string l) {
11716 		label_ = l;
11717 		version(win32_widgets) {
11718 			WCharzBuffer bfr = WCharzBuffer(l);
11719 			SetWindowTextW(hwnd, bfr.ptr);
11720 		} else version(custom_widgets) {
11721 			redraw();
11722 		}
11723 	}
11724 
11725 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
11726 		super.defaultEventHandler_dblclick(ev);
11727 		if(triggersOnMultiClick) {
11728 			if(ev.button == MouseButton.left) {
11729 				auto event = new Event(EventType.triggered, this);
11730 				event.sendDirectly();
11731 			}
11732 		}
11733 	}
11734 
11735 	private Sprite sprite;
11736 	private int displayFlags;
11737 
11738 	/++
11739 		Creates a push button with the given label, which may be an image or some text.
11740 
11741 		Bugs:
11742 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
11743 
11744 		History:
11745 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
11746 
11747 			The button with label and image will respect requests to show both on Windows as
11748 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
11749 	+/
11750 	this(ImageLabel label, Widget parent) {
11751 		version(win32_widgets) {
11752 			// FIXME: use ideal button size instead
11753 			width = 50;
11754 			height = 30;
11755 			super(parent);
11756 
11757 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
11758 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
11759 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
11760 
11761 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
11762 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
11763 
11764 			if(label.image) {
11765 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
11766 
11767 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
11768 			}
11769 
11770 			this.label = label.label;
11771 		} else version(custom_widgets) {
11772 			width = 50;
11773 			height = 30;
11774 			super(parent);
11775 
11776 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
11777 
11778 			if(label.image) {
11779 				this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
11780 				this.displayFlags = label.displayFlags;
11781 			}
11782 
11783 			this.alignment = label.alignment;
11784 		}
11785 	}
11786 
11787 	///
11788 	this(string label, Widget parent) {
11789 		this(ImageLabel(label), parent);
11790 	}
11791 
11792 	override int minHeight() { return defaultLineHeight + 4; }
11793 
11794 	static class Style : Widget.Style {
11795 		override WidgetBackground background() {
11796 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
11797 
11798 			auto pressed = DynamicState.depressed | DynamicState.hover;
11799 			if((widget.dynamicState & pressed) == pressed) {
11800 				return WidgetBackground(cs.depressedButtonColor());
11801 			} else if(widget.dynamicState & DynamicState.hover) {
11802 				return WidgetBackground(cs.hoveringColor());
11803 			} else {
11804 				return WidgetBackground(cs.buttonColor());
11805 			}
11806 		}
11807 
11808 		override FrameStyle borderStyle() {
11809 			auto pressed = DynamicState.depressed | DynamicState.hover;
11810 			if((widget.dynamicState & pressed) == pressed) {
11811 				return FrameStyle.sunk;
11812 			} else {
11813 				return FrameStyle.risen;
11814 			}
11815 
11816 		}
11817 
11818 		override bool variesWithState(ulong dynamicStateFlags) {
11819 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11820 		}
11821 	}
11822 	mixin OverrideStyle!Style;
11823 
11824 	version(custom_widgets)
11825 	override void paint(WidgetPainter painter) {
11826 		painter.drawThemed(delegate Rectangle(const Rectangle bounds) {
11827 			if(sprite) {
11828 				sprite.drawAt(
11829 					painter,
11830 					bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
11831 					Point(0, 0)
11832 				);
11833 			} else {
11834 				painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
11835 			}
11836 			return bounds;
11837 		});
11838 	}
11839 
11840 	override int flexBasisWidth() {
11841 		version(win32_widgets) {
11842 			SIZE size;
11843 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11844 			if(size.cx == 0)
11845 				goto fallback;
11846 			return size.cx + scaleWithDpi(16);
11847 		}
11848 		fallback:
11849 			return scaleWithDpi(cast(int) label.length * 8 + 16);
11850 	}
11851 
11852 	override int flexBasisHeight() {
11853 		version(win32_widgets) {
11854 			SIZE size;
11855 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11856 			if(size.cy == 0)
11857 				goto fallback;
11858 			return size.cy + scaleWithDpi(6);
11859 		}
11860 		fallback:
11861 			return defaultLineHeight + 4;
11862 	}
11863 }
11864 
11865 /++
11866 	A button with a consistent size, suitable for user commands like OK and CANCEL.
11867 +/
11868 class CommandButton : Button {
11869 	this(string label, Widget parent) {
11870 		super(label, parent);
11871 	}
11872 
11873 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
11874 
11875 	override int maxHeight() {
11876 		return defaultLineHeight + 4;
11877 	}
11878 
11879 	override int maxWidth() {
11880 		return defaultLineHeight * 4;
11881 	}
11882 
11883 	override int marginLeft() { return 12; }
11884 	override int marginRight() { return 12; }
11885 	override int marginTop() { return 12; }
11886 	override int marginBottom() { return 12; }
11887 }
11888 
11889 ///
11890 enum ArrowDirection {
11891 	left, ///
11892 	right, ///
11893 	up, ///
11894 	down ///
11895 }
11896 
11897 ///
11898 version(custom_widgets)
11899 class ArrowButton : Button {
11900 	///
11901 	this(ArrowDirection direction, Widget parent) {
11902 		super("", parent);
11903 		this.direction = direction;
11904 		triggersOnMultiClick = true;
11905 	}
11906 
11907 	private ArrowDirection direction;
11908 
11909 	override int minHeight() { return scaleWithDpi(16); }
11910 	override int maxHeight() { return scaleWithDpi(16); }
11911 	override int minWidth() { return scaleWithDpi(16); }
11912 	override int maxWidth() { return scaleWithDpi(16); }
11913 
11914 	override void paint(WidgetPainter painter) {
11915 		super.paint(painter);
11916 
11917 		auto cs = getComputedStyle();
11918 
11919 		painter.outlineColor = cs.foregroundColor;
11920 		painter.fillColor = cs.foregroundColor;
11921 
11922 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
11923 
11924 		final switch(direction) {
11925 			case ArrowDirection.up:
11926 				painter.drawPolygon(
11927 					scaleWithDpi(Point(2, 10) + offset),
11928 					scaleWithDpi(Point(7, 5) + offset),
11929 					scaleWithDpi(Point(12, 10) + offset),
11930 					scaleWithDpi(Point(2, 10) + offset)
11931 				);
11932 			break;
11933 			case ArrowDirection.down:
11934 				painter.drawPolygon(
11935 					scaleWithDpi(Point(2, 6) + offset),
11936 					scaleWithDpi(Point(7, 11) + offset),
11937 					scaleWithDpi(Point(12, 6) + offset),
11938 					scaleWithDpi(Point(2, 6) + offset)
11939 				);
11940 			break;
11941 			case ArrowDirection.left:
11942 				painter.drawPolygon(
11943 					scaleWithDpi(Point(10, 2) + offset),
11944 					scaleWithDpi(Point(5, 7) + offset),
11945 					scaleWithDpi(Point(10, 12) + offset),
11946 					scaleWithDpi(Point(10, 2) + offset)
11947 				);
11948 			break;
11949 			case ArrowDirection.right:
11950 				painter.drawPolygon(
11951 					scaleWithDpi(Point(6, 2) + offset),
11952 					scaleWithDpi(Point(11, 7) + offset),
11953 					scaleWithDpi(Point(6, 12) + offset),
11954 					scaleWithDpi(Point(6, 2) + offset)
11955 				);
11956 			break;
11957 		}
11958 	}
11959 }
11960 
11961 private
11962 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
11963 	int x, y;
11964 	Widget par = c;
11965 	while(par) {
11966 		x += par.x;
11967 		y += par.y;
11968 		par = par.parent;
11969 	}
11970 	return [x, y];
11971 }
11972 
11973 version(win32_widgets)
11974 private
11975 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
11976 // MapWindowPoints?
11977 	int x, y;
11978 	Widget par = c;
11979 	while(par) {
11980 		x += par.x;
11981 		y += par.y;
11982 		par = par.parent;
11983 		if(par !is null && par.useNativeDrawing())
11984 			break;
11985 	}
11986 	return [x, y];
11987 }
11988 
11989 ///
11990 class ImageBox : Widget {
11991 	private MemoryImage image_;
11992 
11993 	override int widthStretchiness() { return 1; }
11994 	override int heightStretchiness() { return 1; }
11995 	override int widthShrinkiness() { return 1; }
11996 	override int heightShrinkiness() { return 1; }
11997 
11998 	override int flexBasisHeight() {
11999 		return image_.height;
12000 	}
12001 
12002 	override int flexBasisWidth() {
12003 		return image_.width;
12004 	}
12005 
12006 	///
12007 	public void setImage(MemoryImage image){
12008 		this.image_ = image;
12009 		if(this.parentWindow && this.parentWindow.win) {
12010 			if(sprite)
12011 				sprite.dispose();
12012 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
12013 		}
12014 		redraw();
12015 	}
12016 
12017 	/// How to fit the image in the box if they aren't an exact match in size?
12018 	enum HowToFit {
12019 		center, /// centers the image, cropping around all the edges as needed
12020 		crop, /// always draws the image in the upper left, cropping the lower right if needed
12021 		// stretch, /// not implemented
12022 	}
12023 
12024 	private Sprite sprite;
12025 	private HowToFit howToFit_;
12026 
12027 	private Color backgroundColor_;
12028 
12029 	///
12030 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
12031 		this.image_ = image;
12032 		this.tabStop = false;
12033 		this.howToFit_ = howToFit;
12034 		this.backgroundColor_ = backgroundColor;
12035 		super(parent);
12036 		updateSprite();
12037 	}
12038 
12039 	/// ditto
12040 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
12041 		this(image, howToFit, Color.transparent, parent);
12042 	}
12043 
12044 	private void updateSprite() {
12045 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
12046 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
12047 		}
12048 	}
12049 
12050 	override void paint(WidgetPainter painter) {
12051 		updateSprite();
12052 		if(backgroundColor_.a) {
12053 			painter.fillColor = backgroundColor_;
12054 			painter.drawRectangle(Point(0, 0), width, height);
12055 		}
12056 		if(howToFit_ == HowToFit.crop)
12057 			sprite.drawAt(painter, Point(0, 0));
12058 		else if(howToFit_ == HowToFit.center) {
12059 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
12060 		}
12061 	}
12062 }
12063 
12064 ///
12065 class TextLabel : Widget {
12066 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
12067 	override int maxHeight() { return minHeight; }
12068 	override int minWidth() { return 32; }
12069 
12070 	override int flexBasisHeight() { return minHeight(); }
12071 	override int flexBasisWidth() { return defaultTextWidth(label); }
12072 
12073 	string label_;
12074 
12075 	/++
12076 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
12077 
12078 		In practice this means a click on the label will focus the `labelFor`. In future versions
12079 		it will also set screen reader hints but that is not yet implemented.
12080 
12081 		History:
12082 			Added October 3, 2021 (dub v10.4)
12083 	+/
12084 	Widget labelFor;
12085 
12086 	///
12087 	@scriptable
12088 	string label() { return label_; }
12089 
12090 	///
12091 	@scriptable
12092 	void label(string l) {
12093 		label_ = l;
12094 		version(win32_widgets) {
12095 			WCharzBuffer bfr = WCharzBuffer(l);
12096 			SetWindowTextW(hwnd, bfr.ptr);
12097 		} else version(custom_widgets)
12098 			redraw();
12099 	}
12100 
12101 	override void defaultEventHandler_click(scope ClickEvent ce) {
12102 		if(this.labelFor !is null)
12103 			this.labelFor.focus();
12104 	}
12105 
12106 	/++
12107 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
12108 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
12109 	+/
12110 	this(string label, TextAlignment alignment, Widget parent) {
12111 		this.label_ = label;
12112 		this.alignment = alignment;
12113 		this.tabStop = false;
12114 		super(parent);
12115 
12116 		version(win32_widgets)
12117 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
12118 	}
12119 
12120 	/// ditto
12121 	this(string label, Widget parent) {
12122 		this(label, TextAlignment.Right, parent);
12123 	}
12124 
12125 	TextAlignment alignment;
12126 
12127 	version(custom_widgets)
12128 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12129 		painter.outlineColor = getComputedStyle().foregroundColor;
12130 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
12131 		return bounds;
12132 	}
12133 
12134 }
12135 
12136 version(custom_widgets)
12137 	private struct etc {
12138 		mixin ExperimentalTextComponent;
12139 	}
12140 
12141 version(win32_widgets) {
12142 	alias EditableTextWidgetParent = Widget; ///
12143 	version=use_new_text_system;
12144 	import arsd.textlayouter;
12145 } else version(custom_widgets) {
12146 	version(trash_text) {
12147 		alias EditableTextWidgetParent = ScrollableWidget; ///
12148 	} else {
12149 		alias EditableTextWidgetParent = Widget;
12150 		version=use_new_text_system;
12151 		import arsd.textlayouter;
12152 	}
12153 } else static assert(0);
12154 
12155 version(use_new_text_system)
12156 class TextDisplayHelper : Widget {
12157 	protected TextLayouter l;
12158 	protected ScrollMessageWidget smw;
12159 
12160 	private const(TextLayouter.State)*[] undoStack;
12161 	private const(TextLayouter.State)*[] redoStack;
12162 
12163 	private string preservedPrimaryText;
12164 	protected void selectionChanged() {
12165 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
12166 		static if(UsingSimpledisplayX11)
12167 		with(l.selection()) {
12168 			if(!isEmpty()) {
12169 				//sdpyPrintDebugString("!isEmpty");
12170 
12171 				getPrimarySelection(parentWindow.win, (in char[] txt) {
12172 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
12173 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
12174 					if(txt.length) {
12175 						preservedPrimaryText = txt.idup;
12176 						// writeln(preservedPrimaryText);
12177 					}
12178 
12179 					setPrimarySelection(parentWindow.win, getContentString());
12180 				});
12181 			}
12182 		}
12183 	}
12184 
12185 	final TextLayouter layouter() {
12186 		return l;
12187 	}
12188 
12189 	bool readonly;
12190 	bool caretNavigation; // scroll lock can flip this
12191 	bool singleLine;
12192 	bool acceptsTabInput;
12193 
12194 	private Menu ctx;
12195 	override Menu contextMenu(int x, int y) {
12196 		if(ctx is null) {
12197 			ctx = new Menu("Actions", this);
12198 			if(!readonly) {
12199 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
12200 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
12201 				ctx.addSeparator();
12202 			}
12203 			if(!readonly)
12204 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
12205 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
12206 			if(!readonly)
12207 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
12208 			if(!readonly)
12209 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
12210 			ctx.addSeparator();
12211 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
12212 		}
12213 		return ctx;
12214 	}
12215 
12216 	override void defaultEventHandler_blur(Event ev) {
12217 		super.defaultEventHandler_blur(ev);
12218 		if(l.wasMutated()) {
12219 			auto evt = new ChangeEvent!string(this, &this.content);
12220 			evt.dispatch();
12221 			l.clearWasMutatedFlag();
12222 		}
12223 	}
12224 
12225 	private string content() {
12226 		return l.getTextString();
12227 	}
12228 
12229 	void undo() {
12230 		if(readonly) return;
12231 		if(undoStack.length) {
12232 			auto state = undoStack[$-1];
12233 			undoStack = undoStack[0 .. $-1];
12234 			undoStack.assumeSafeAppend();
12235 			redoStack ~= l.saveState();
12236 			l.restoreState(state);
12237 			adjustScrollbarSizes();
12238 			scrollForCaret();
12239 			redraw();
12240 			stateCheckpoint = true;
12241 		}
12242 	}
12243 
12244 	void redo() {
12245 		if(readonly) return;
12246 		if(redoStack.length) {
12247 			doStateCheckpoint();
12248 			auto state = redoStack[$-1];
12249 			redoStack = redoStack[0 .. $-1];
12250 			redoStack.assumeSafeAppend();
12251 			l.restoreState(state);
12252 			adjustScrollbarSizes();
12253 			scrollForCaret();
12254 			redraw();
12255 			stateCheckpoint = true;
12256 		}
12257 	}
12258 
12259 	void cut() {
12260 		if(readonly) return;
12261 		with(l.selection()) {
12262 			if(!isEmpty()) {
12263 				setClipboardText(parentWindow.win, getContentString());
12264 				doStateCheckpoint();
12265 				replaceContent("");
12266 				adjustScrollbarSizes();
12267 				scrollForCaret();
12268 				this.redraw();
12269 			}
12270 		}
12271 
12272 	}
12273 
12274 	void copy() {
12275 		with(l.selection()) {
12276 			if(!isEmpty()) {
12277 				setClipboardText(parentWindow.win, getContentString());
12278 				this.redraw();
12279 			}
12280 		}
12281 	}
12282 
12283 	void paste() {
12284 		if(readonly) return;
12285 		getClipboardText(parentWindow.win, (txt) {
12286 			doStateCheckpoint();
12287 			if(singleLine)
12288 				l.selection.replaceContent(txt.stripInternal());
12289 			else
12290 				l.selection.replaceContent(txt);
12291 			adjustScrollbarSizes();
12292 			scrollForCaret();
12293 			this.redraw();
12294 		});
12295 	}
12296 
12297 	void deleteContentOfSelection() {
12298 		if(readonly) return;
12299 		doStateCheckpoint();
12300 		l.selection.replaceContent("");
12301 		l.selection.setUserXCoordinate();
12302 		adjustScrollbarSizes();
12303 		scrollForCaret();
12304 		redraw();
12305 	}
12306 
12307 	void selectAll() {
12308 		with(l.selection) {
12309 			moveToStartOfDocument();
12310 			setAnchor();
12311 			moveToEndOfDocument();
12312 			setFocus();
12313 
12314 			selectionChanged();
12315 		}
12316 		redraw();
12317 	}
12318 
12319 	protected bool stateCheckpoint = true;
12320 
12321 	protected void doStateCheckpoint() {
12322 		if(stateCheckpoint) {
12323 			undoStack ~= l.saveState();
12324 			stateCheckpoint = false;
12325 		}
12326 	}
12327 
12328 	protected void adjustScrollbarSizes() {
12329 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
12330 		auto borderWidth = 2;
12331 		this.smw.setTotalArea(l.width, l.height);
12332 		this.smw.setViewableArea(
12333 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
12334 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
12335 	}
12336 
12337 	protected void scrollForCaret() {
12338 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
12339 		smw.scrollIntoView(l.selection.focusBoundingBox());
12340 	}
12341 
12342 	// FIXME: this should be a theme changed event listener instead
12343 	private BaseVisualTheme currentTheme;
12344 	override void recomputeChildLayout() {
12345 		if(currentTheme is null)
12346 			currentTheme = WidgetPainter.visualTheme;
12347 		if(WidgetPainter.visualTheme !is currentTheme) {
12348 			currentTheme = WidgetPainter.visualTheme;
12349 			auto ds = this.l.defaultStyle;
12350 			if(auto ms = cast(MyTextStyle) ds) {
12351 				auto cs = getComputedStyle();
12352 				auto font = cs.font();
12353 				if(font !is null)
12354 					ms.font_ = font;
12355 				else {
12356 					auto osc = new OperatingSystemFont();
12357 					osc.loadDefault;
12358 					ms.font_ = osc;
12359 				}
12360 			}
12361 		}
12362 		super.recomputeChildLayout();
12363 	}
12364 
12365 	private Point adjustForSingleLine(Point p) {
12366 		if(singleLine)
12367 			return Point(p.x, this.height / 2);
12368 		else
12369 			return p;
12370 	}
12371 
12372 	private bool wordWrapEnabled_;
12373 
12374 	this(TextLayouter l, ScrollMessageWidget parent) {
12375 		this.smw = parent;
12376 
12377 		smw.addDefaultWheelListeners(16, 16, 8);
12378 		smw.movementPerButtonClick(16, 16);
12379 
12380 		this.defaultPadding = Rectangle(2, 2, 2, 2);
12381 
12382 		this.l = l;
12383 		super(parent);
12384 
12385 		smw.addEventListener((scope ScrollEvent se) {
12386 			this.redraw();
12387 		});
12388 
12389 		bool mouseDown;
12390 		bool mouseActuallyMoved;
12391 
12392 		this.addEventListener((scope ResizeEvent re) {
12393 			// FIXME: I should add a method to give this client area width thing
12394 			if(wordWrapEnabled_)
12395 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
12396 
12397 			adjustScrollbarSizes();
12398 			scrollForCaret();
12399 
12400 			this.redraw();
12401 		});
12402 
12403 		this.addEventListener((scope KeyDownEvent kde) {
12404 			switch(kde.key) {
12405 				case Key.Up, Key.Down, Key.Left, Key.Right:
12406 				case Key.Home, Key.End:
12407 					stateCheckpoint = true;
12408 					bool setPosition = false;
12409 					switch(kde.key) {
12410 						case Key.Up: l.selection.moveUp(); break;
12411 						case Key.Down: l.selection.moveDown(); break;
12412 						case Key.Left: l.selection.moveLeft(); setPosition = true; break;
12413 						case Key.Right: l.selection.moveRight(); setPosition = true; break;
12414 						case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
12415 						case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
12416 						default: assert(0);
12417 					}
12418 
12419 					if(kde.shiftKey)
12420 						l.selection.setFocus();
12421 					else
12422 						l.selection.setAnchor();
12423 
12424 					selectionChanged();
12425 
12426 					if(setPosition)
12427 						l.selection.setUserXCoordinate();
12428 					scrollForCaret();
12429 					redraw();
12430 				break;
12431 				case Key.PageUp, Key.PageDown:
12432 					// FIXME
12433 					scrollForCaret();
12434 				break;
12435 				case Key.Delete:
12436 					if(l.selection.isEmpty()) {
12437 						l.selection.setAnchor();
12438 						l.selection.moveRight();
12439 						l.selection.setFocus();
12440 					}
12441 					deleteContentOfSelection();
12442 					adjustScrollbarSizes();
12443 					scrollForCaret();
12444 				break;
12445 				case Key.Insert:
12446 				break;
12447 				case Key.A:
12448 					if(kde.ctrlKey)
12449 						selectAll();
12450 				break;
12451 				case Key.F:
12452 					// find
12453 				break;
12454 				case Key.Z:
12455 					if(kde.ctrlKey)
12456 						undo();
12457 				break;
12458 				case Key.R:
12459 					if(kde.ctrlKey)
12460 						redo();
12461 				break;
12462 				case Key.X:
12463 					if(kde.ctrlKey)
12464 						cut();
12465 				break;
12466 				case Key.C:
12467 					if(kde.ctrlKey)
12468 						copy();
12469 				break;
12470 				case Key.V:
12471 					if(kde.ctrlKey)
12472 						paste();
12473 				break;
12474 				case Key.F1:
12475 					with(l.selection()) {
12476 						moveToStartOfLine();
12477 						setAnchor();
12478 						moveToEndOfLine();
12479 						moveToIncludeAdjacentEndOfLineMarker();
12480 						setFocus();
12481 						replaceContent("");
12482 					}
12483 
12484 					redraw();
12485 				break;
12486 				/*
12487 				case Key.F2:
12488 					l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
12489 						//(cast(MyTextStyle) old).font,
12490 						font2,
12491 						Color.red)));
12492 					redraw();
12493 				break;
12494 				*/
12495 				case Key.Tab:
12496 					// we process the char event, so don't want to change focus on it
12497 					if(acceptsTabInput)
12498 						kde.preventDefault();
12499 				break;
12500 				default:
12501 			}
12502 		});
12503 
12504 		Point downAt;
12505 
12506 		static if(UsingSimpledisplayX11)
12507 		this.addEventListener((scope ClickEvent ce) {
12508 			if(ce.button == MouseButton.middle) {
12509 				parentWindow.win.getPrimarySelection((txt) {
12510 					doStateCheckpoint();
12511 
12512 					// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
12513 
12514 					if(txt == l.selection.getContentString && preservedPrimaryText.length)
12515 						l.selection.replaceContent(preservedPrimaryText);
12516 					else
12517 						l.selection.replaceContent(txt);
12518 					redraw();
12519 				});
12520 			}
12521 		});
12522 
12523 		this.addEventListener((scope DoubleClickEvent dce) {
12524 			if(dce.button == MouseButton.left) {
12525 				with(l.selection()) {
12526 					scope dg = delegate const(char)[] (scope return const(char)[] ch) {
12527 						if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
12528 							return ch;
12529 						return null;
12530 					};
12531 					find(dg, 1, true).moveToEnd.setAnchor;
12532 					find(dg, 1, false).moveTo.setFocus;
12533 					selectionChanged();
12534 					redraw();
12535 				}
12536 			}
12537 		});
12538 
12539 		this.addEventListener((scope MouseDownEvent ce) {
12540 			if(ce.button == MouseButton.left) {
12541 				downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12542 				l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
12543 				l.selection.setAnchor();
12544 				mouseDown = true;
12545 				mouseActuallyMoved = false;
12546 				parentWindow.captureMouse(this);
12547 				this.redraw();
12548 			} else if(ce.button == MouseButton.right) {
12549 				this.showContextMenu(ce.clientX, ce.clientY);
12550 			}
12551 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12552 		});
12553 
12554 		Timer autoscrollTimer;
12555 		int autoscrollDirection;
12556 		int autoscrollAmount;
12557 
12558 		void autoscroll() {
12559 			switch(autoscrollDirection) {
12560 				case 0: smw.scrollUp(autoscrollAmount); break;
12561 				case 1: smw.scrollDown(autoscrollAmount); break;
12562 				case 2: smw.scrollLeft(autoscrollAmount); break;
12563 				case 3: smw.scrollRight(autoscrollAmount); break;
12564 				default: assert(0);
12565 			}
12566 
12567 			this.redraw();
12568 		}
12569 
12570 		void setAutoscrollTimer(int direction, int amount) {
12571 			if(autoscrollTimer is null) {
12572 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
12573 			}
12574 
12575 			autoscrollDirection = direction;
12576 			autoscrollAmount = amount;
12577 		}
12578 
12579 		void stopAutoscrollTimer() {
12580 			if(autoscrollTimer !is null) {
12581 				autoscrollTimer.dispose();
12582 				autoscrollTimer = null;
12583 			}
12584 			autoscrollAmount = 0;
12585 			autoscrollDirection = 0;
12586 		}
12587 
12588 		this.addEventListener((scope MouseMoveEvent ce) {
12589 			if(mouseDown) {
12590 				auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12591 
12592 				// FIXME: when scrolling i actually do want a timer.
12593 				// i also want a zone near the sides of the window where i can auto scroll
12594 
12595 				auto scrollMultiplier = scaleWithDpi(16);
12596 				auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
12597 
12598 				if(!singleLine && movedTo.y < 4) {
12599 					setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
12600 				} else
12601 				if(!singleLine && (movedTo.y + 6) > this.height) {
12602 					setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
12603 				} else
12604 				if(movedTo.x < 4) {
12605 					setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
12606 				} else
12607 				if((movedTo.x + 6) > this.width) {
12608 					setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
12609 				} else
12610 					stopAutoscrollTimer();
12611 
12612 				l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
12613 				l.selection.setFocus();
12614 				mouseActuallyMoved = true;
12615 				this.redraw();
12616 			}
12617 		});
12618 
12619 		this.addEventListener((scope MouseUpEvent ce) {
12620 			// FIXME: assert primary selection
12621 			if(mouseDown && ce.button == MouseButton.left) {
12622 				stateCheckpoint = true;
12623 				//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
12624 				//l.selection.setFocus();
12625 				mouseDown = false;
12626 				parentWindow.releaseMouseCapture();
12627 				stopAutoscrollTimer();
12628 				this.redraw();
12629 
12630 				if(mouseActuallyMoved)
12631 					selectionChanged();
12632 			}
12633 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
12634 		});
12635 
12636 		this.addEventListener((scope CharEvent ce) {
12637 			if(readonly)
12638 				return;
12639 			if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
12640 				return; // skip the ctrl+x characters we don't care about as plain text
12641 
12642 			if(singleLine && ce.character == '\n')
12643 				return;
12644 			if(!acceptsTabInput && ce.character == '\t')
12645 				return;
12646 
12647 			doStateCheckpoint();
12648 
12649 			char[4] buffer;
12650 			import arsd.core;
12651 			auto stride = encodeUtf8(buffer, ce.character);
12652 			l.selection.replaceContent(buffer[0 .. stride]);
12653 			l.selection.setUserXCoordinate();
12654 			adjustScrollbarSizes();
12655 			scrollForCaret();
12656 			redraw();
12657 		});
12658 	}
12659 
12660 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
12661 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
12662 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidgetParent
12663 		if(parent && parent.parent && parent.parent.parent)
12664 			parent.parent.parent.useStyleProperties(dg);
12665 		else
12666 			super.useStyleProperties(dg);
12667 	}
12668 
12669 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
12670 	override int maxHeight() {
12671 		if(singleLine)
12672 			return minHeight;
12673 		else
12674 			return super.maxHeight();
12675 	}
12676 
12677 	void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
12678 		painter.drawText(upperLeft, text);
12679 	}
12680 
12681 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12682 		//painter.setFont(font);
12683 
12684 		auto cs = getComputedStyle();
12685 		auto defaultColor = cs.foregroundColor;
12686 
12687 		auto old = painter.setClipRectangle(bounds);
12688 		scope(exit) painter.setClipRectangle(old);
12689 
12690 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
12691 			//writeln("Segment: ", txt);
12692 			assert(style !is null);
12693 
12694 			auto myStyle = cast(MyTextStyle) style;
12695 			assert(myStyle !is null);
12696 
12697 			painter.setFont(myStyle.font);
12698 			// defaultColor = myStyle.color; // FIXME: so wrong
12699 
12700 			if(info.selections && info.boundingBox.width > 0) {
12701 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
12702 				painter.fillColor = color;
12703 				painter.outlineColor = color;
12704 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
12705 				painter.outlineColor = cs.selectionForegroundColor;
12706 				//painter.fillColor = Color.white;
12707 			} else {
12708 				painter.outlineColor = defaultColor;
12709 			}
12710 
12711 			if(this.isFocused)
12712 			foreach(idx, caret; carets) {
12713 				if(idx == 0)
12714 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
12715 				painter.drawLine(
12716 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
12717 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
12718 				);
12719 			}
12720 
12721 			if(txt.stripInternal.length) {
12722 				drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
12723 			}
12724 
12725 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
12726 				return false;
12727 			} else {
12728 				return true;
12729 			}
12730 		}, Rectangle(smw.position(), bounds.size));
12731 
12732 		/+
12733 		int place = 0;
12734 		int y = 75;
12735 		foreach(width; widths) {
12736 			painter.fillColor = Color.red;
12737 			painter.drawRectangle(Point(place, y), Size(width, 75));
12738 			//y += 15;
12739 			place += width;
12740 		}
12741 		+/
12742 
12743 		return bounds;
12744 	}
12745 
12746 	static class MyTextStyle : TextStyle {
12747 		OperatingSystemFont font_;
12748 		this(OperatingSystemFont font, bool passwordMode = false) {
12749 			this.font_ = font;
12750 		}
12751 
12752 		override OperatingSystemFont font() {
12753 			return font_;
12754 		}
12755 	}
12756 }
12757 
12758 /+
12759 version(use_new_text_system)
12760 class TextWidget : Widget {
12761 	TextLayouter l;
12762 	ScrollMessageWidget smw;
12763 	TextDisplayHelper helper;
12764 	this(TextLayouter l, Widget parent) {
12765 		this.l = l;
12766 		super(parent);
12767 
12768 		smw = new ScrollMessageWidget(this);
12769 		//smw.horizontalScrollBar.hide;
12770 		//smw.verticalScrollBar.hide;
12771 		smw.addDefaultWheelListeners(16, 16, 8);
12772 		smw.movementPerButtonClick(16, 16);
12773 		helper = new TextDisplayHelper(l, smw);
12774 
12775 		// no need to do this here since there's gonna be a resize
12776 		// event immediately before any drawing
12777 		// smw.setTotalArea(l.width, l.height);
12778 		smw.setViewableArea(
12779 			this.width - this.paddingLeft - this.paddingRight,
12780 			this.height - this.paddingTop - this.paddingBottom);
12781 
12782 		/+
12783 		writeln(l.width, "x", l.height);
12784 		+/
12785 	}
12786 }
12787 +/
12788 
12789 
12790 
12791 
12792 /+
12793 	This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too
12794 +/
12795 
12796 /// Contains the implementation of text editing
12797 abstract class EditableTextWidget : EditableTextWidgetParent {
12798 	this(Widget parent) {
12799 		version(custom_widgets)
12800 			this(true, parent);
12801 		else
12802 			this(false, parent);
12803 	}
12804 
12805 	private bool useCustomWidget;
12806 
12807 	this(bool useCustomWidget, Widget parent) {
12808 		this.useCustomWidget = useCustomWidget;
12809 
12810 		super(parent);
12811 
12812 		if(useCustomWidget)
12813 			setupCustomTextEditing();
12814 	}
12815 
12816 	private bool wordWrapEnabled_;
12817 	void wordWrapEnabled(bool enabled) {
12818 		if(useCustomWidget) {
12819 			wordWrapEnabled_ = enabled;
12820 			version(use_new_text_system)
12821 				textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
12822 		} else version(win32_widgets) {
12823 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
12824 		}
12825 	}
12826 
12827 	override int minWidth() { return scaleWithDpi(16); }
12828 	override int widthStretchiness() { return 7; }
12829 	override int widthShrinkiness() { return 1; }
12830 
12831 	version(use_new_text_system)
12832 	override int maxHeight() {
12833 		if(useCustomWidget)
12834 			return tdh.maxHeight;
12835 		else
12836 			return super.maxHeight();
12837 	}
12838 
12839 	version(use_new_text_system)
12840 	override void focus() {
12841 		if(useCustomWidget && tdh)
12842 			tdh.focus();
12843 		else
12844 			super.focus();
12845 	}
12846 
12847 	void selectAll() {
12848 		if(useCustomWidget) {
12849 			version(use_new_text_system)
12850 				tdh.selectAll();
12851 			else version(trash_text)
12852 				textLayout.selectAll();
12853 			redraw();
12854 		} else version(win32_widgets) {
12855 			SendMessage(hwnd, EM_SETSEL, 0, -1);
12856 		}
12857 	}
12858 
12859 	version(use_new_text_system)
12860 		TextDisplayHelper tdh;
12861 
12862 	@property string content() {
12863 		if(useCustomWidget) {
12864 			version(use_new_text_system) {
12865 				return textLayout.getTextString();
12866 			} else version(trash_text) {
12867 				return textLayout.getPlainText();
12868 			}
12869 		} else version(win32_widgets) {
12870 			wchar[4096] bufferstack;
12871 			wchar[] buffer;
12872 			auto len = GetWindowTextLength(hwnd);
12873 			if(len < bufferstack.length)
12874 				buffer = bufferstack[0 .. len + 1];
12875 			else
12876 				buffer = new wchar[](len + 1);
12877 
12878 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
12879 			if(l >= 0)
12880 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
12881 			else
12882 				return null;
12883 		}
12884 
12885 		assert(0);
12886 	}
12887 	@property void content(string s) {
12888 		if(useCustomWidget) {
12889 			version(use_new_text_system) {
12890 				with(textLayout.selection) {
12891 					moveToStartOfDocument();
12892 					setAnchor();
12893 					moveToEndOfDocument();
12894 					setFocus();
12895 					replaceContent(s);
12896 				}
12897 
12898 				tdh.adjustScrollbarSizes();
12899 				// these don't seem to help
12900 				// tdh.smw.setPosition(0, 0);
12901 				// tdh.scrollForCaret();
12902 
12903 				redraw();
12904 			} else version(trash_text) {
12905 				textLayout.clear();
12906 				textLayout.addText(s);
12907 
12908 				{
12909 				// FIXME: it should be able to get this info easier
12910 				auto painter = draw();
12911 				textLayout.redoLayout(painter);
12912 				}
12913 				auto cbb = textLayout.contentBoundingBox();
12914 				setContentSize(cbb.width, cbb.height);
12915 				/*
12916 				textLayout.addText(ForegroundColor.red, s);
12917 				textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/");
12918 				textLayout.addText(" is the best!");
12919 				*/
12920 				redraw();
12921 			}
12922 		} else version(win32_widgets) {
12923 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
12924 			SetWindowTextW(hwnd, bfr.ptr);
12925 		}
12926 	}
12927 
12928 	void addText(string txt) {
12929 		if(useCustomWidget) {
12930 			version(use_new_text_system) {
12931 				textLayout.appendText(txt);
12932 				tdh.adjustScrollbarSizes();
12933 				redraw();
12934 			} else if(trash_text) {
12935 				textLayout.addText(txt);
12936 
12937 				{
12938 				// FIXME: it should be able to get this info easier
12939 				auto painter = draw();
12940 				textLayout.redoLayout(painter);
12941 				}
12942 				auto cbb = textLayout.contentBoundingBox();
12943 				setContentSize(cbb.width, cbb.height);
12944 			}
12945 		} else version(win32_widgets) {
12946 			// get the current selection
12947 			DWORD StartPos, EndPos;
12948 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
12949 
12950 			// move the caret to the end of the text
12951 			int outLength = GetWindowTextLengthW(hwnd);
12952 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
12953 
12954 			// insert the text at the new caret position
12955 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
12956 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
12957 
12958 			// restore the previous selection
12959 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
12960 		}
12961 	}
12962 
12963 	version(custom_widgets)
12964 	version(trash_text)
12965 	override void paintFrameAndBackground(WidgetPainter painter) {
12966 		this.draw3dFrame(painter, FrameStyle.sunk, Color.white);
12967 	}
12968 
12969 	version(use_new_text_system)
12970 	TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
12971 		return new TextDisplayHelper(textLayout, smw);
12972 	}
12973 
12974 	version(use_new_text_system)
12975 	TextStyle defaultTextStyle() {
12976 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
12977 	}
12978 
12979 	version(use_new_text_system)
12980 	private OperatingSystemFont getUsedFont() {
12981 		auto cs = getComputedStyle();
12982 		auto font = cs.font;
12983 		if(font is null) {
12984 			font = new OperatingSystemFont;
12985 			font.loadDefault();
12986 		}
12987 		return font;
12988 	}
12989 
12990 	version(use_new_text_system) {
12991 		TextLayouter textLayout;
12992 
12993 		void setupCustomTextEditing() {
12994 			textLayout = new TextLayouter(defaultTextStyle());
12995 
12996 			auto smw = new ScrollMessageWidget(this);
12997 			if(!showingHorizontalScroll)
12998 				smw.horizontalScrollBar.hide();
12999 			if(!showingVerticalScroll)
13000 				smw.verticalScrollBar.hide();
13001 			this.tabStop = false;
13002 			smw.tabStop = false;
13003 			tdh = textDisplayHelperFactory(textLayout, smw);
13004 		}
13005 
13006 		override void newParentWindow(Window old, Window n) {
13007 			if(n is null) return;
13008 			this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
13009 				if(textLayout) {
13010 					if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
13011 						// the dpi change can change the font, so this informs the layouter that it has changed too
13012 						style.font_ = getUsedFont();
13013 
13014 						// arsd.core.writeln(this.parentWindow.win.actualDpi);
13015 					}
13016 				}
13017 			});
13018 		}
13019 
13020 	} else version(trash_text) {
13021 		static if(SimpledisplayTimerAvailable)
13022 			Timer caretTimer;
13023 		etc.TextLayout textLayout;
13024 
13025 		void setupCustomTextEditing() {
13026 			textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4));
13027 			textLayout.selectionXorColor = getComputedStyle().activeListXorColor;
13028 		}
13029 
13030 		override void paint(WidgetPainter painter) {
13031 			if(parentWindow.win.closed) return;
13032 
13033 			textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4);
13034 
13035 			/*
13036 			painter.outlineColor = Color.white;
13037 			painter.fillColor = Color.white;
13038 			painter.drawRectangle(Point(4, 4), contentWidth, contentHeight);
13039 			*/
13040 
13041 			painter.outlineColor = Color.black;
13042 			// painter.drawText(Point(4, 4), content, Point(width - 4, height - 4));
13043 
13044 			textLayout.caretShowingOnScreen = false;
13045 
13046 			textLayout.drawInto(painter, !parentWindow.win.closed && isFocused());
13047 		}
13048 	}
13049 
13050 	static class Style : Widget.Style {
13051 		override WidgetBackground background() {
13052 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
13053 		}
13054 
13055 		override Color foregroundColor() {
13056 			return WidgetPainter.visualTheme.foregroundColor;
13057 		}
13058 
13059 		override FrameStyle borderStyle() {
13060 			return FrameStyle.sunk;
13061 		}
13062 
13063 		override MouseCursor cursor() {
13064 			return GenericCursor.Text;
13065 		}
13066 	}
13067 	mixin OverrideStyle!Style;
13068 
13069 	version(trash_text)
13070 	version(custom_widgets)
13071 	override void defaultEventHandler_mousedown(MouseDownEvent ev) {
13072 		super.defaultEventHandler_mousedown(ev);
13073 		if(parentWindow.win.closed) return;
13074 		if(ev.button == MouseButton.left) {
13075 			if(textLayout.selectNone())
13076 				redraw();
13077 			textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY);
13078 			this.focus();
13079 			//this.parentWindow.win.grabInput();
13080 		} else if(ev.button == MouseButton.middle) {
13081 			static if(UsingSimpledisplayX11) {
13082 				getPrimarySelection(parentWindow.win, (in char[] txt) {
13083 					textLayout.insert(txt);
13084 					redraw();
13085 
13086 					auto cbb = textLayout.contentBoundingBox();
13087 					setContentSize(cbb.width, cbb.height);
13088 				});
13089 			}
13090 		}
13091 	}
13092 
13093 	version(trash_text)
13094 	version(custom_widgets)
13095 	override void defaultEventHandler_mouseup(MouseUpEvent ev) {
13096 		//this.parentWindow.win.releaseInputGrab();
13097 		super.defaultEventHandler_mouseup(ev);
13098 	}
13099 
13100 	version(trash_text)
13101 	version(custom_widgets)
13102 	override void defaultEventHandler_mousemove(MouseMoveEvent ev) {
13103 		super.defaultEventHandler_mousemove(ev);
13104 		if(ev.state & ModifierState.leftButtonDown) {
13105 			textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY);
13106 			redraw();
13107 		}
13108 	}
13109 
13110 	version(trash_text)
13111 	version(custom_widgets)
13112 	override void defaultEventHandler_focus(Event ev) {
13113 		super.defaultEventHandler_focus(ev);
13114 		if(parentWindow.win.closed) return;
13115 		auto painter = this.draw();
13116 		textLayout.drawCaret(painter);
13117 
13118 		static if(SimpledisplayTimerAvailable)
13119 		if(caretTimer) {
13120 			caretTimer.destroy();
13121 			caretTimer = null;
13122 		}
13123 
13124 		bool blinkingCaret = true;
13125 		static if(UsingSimpledisplayX11)
13126 			if(!Image.impl.xshmAvailable)
13127 				blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink
13128 
13129 		if(blinkingCaret)
13130 		static if(SimpledisplayTimerAvailable)
13131 		caretTimer = new Timer(500, {
13132 			if(parentWindow.win.closed) {
13133 				caretTimer.destroy();
13134 				return;
13135 			}
13136 			if(isFocused()) {
13137 				auto painter = this.draw();
13138 				textLayout.drawCaret(painter);
13139 			} else if(textLayout.caretShowingOnScreen) {
13140 				auto painter = this.draw();
13141 				textLayout.eraseCaret(painter);
13142 			}
13143 		});
13144 	}
13145 
13146 	version(trash_text) {
13147 		private string lastContentBlur;
13148 
13149 		override void defaultEventHandler_blur(Event ev) {
13150 			super.defaultEventHandler_blur(ev);
13151 			if(parentWindow.win.closed) return;
13152 			version(custom_widgets) {
13153 				auto painter = this.draw();
13154 				textLayout.eraseCaret(painter);
13155 				static if(SimpledisplayTimerAvailable)
13156 				if(caretTimer) {
13157 					caretTimer.destroy();
13158 					caretTimer = null;
13159 				}
13160 			}
13161 
13162 			if(this.content != lastContentBlur) {
13163 				auto evt = new ChangeEvent!string(this, &this.content);
13164 				evt.dispatch();
13165 				lastContentBlur = this.content;
13166 			}
13167 		}
13168 	}
13169 
13170 	version(win32_widgets) {
13171 		private string lastContentBlur;
13172 
13173 		override void defaultEventHandler_blur(Event ev) {
13174 			super.defaultEventHandler_blur(ev);
13175 
13176 			if(!useCustomWidget)
13177 			if(this.content != lastContentBlur) {
13178 				auto evt = new ChangeEvent!string(this, &this.content);
13179 				evt.dispatch();
13180 				lastContentBlur = this.content;
13181 			}
13182 		}
13183 	}
13184 
13185 
13186 	version(trash_text)
13187 	version(custom_widgets)
13188 	override void defaultEventHandler_char(CharEvent ev) {
13189 		super.defaultEventHandler_char(ev);
13190 		textLayout.insert(ev.character);
13191 		redraw();
13192 
13193 		// FIXME: too inefficient
13194 		auto cbb = textLayout.contentBoundingBox();
13195 		setContentSize(cbb.width, cbb.height);
13196 	}
13197 	version(trash_text)
13198 	version(custom_widgets)
13199 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
13200 		//super.defaultEventHandler_keydown(ev);
13201 		switch(ev.key) {
13202 			case Key.Delete:
13203 				textLayout.delete_();
13204 				redraw();
13205 			break;
13206 			case Key.Left:
13207 				textLayout.moveLeft();
13208 				redraw();
13209 			break;
13210 			case Key.Right:
13211 				textLayout.moveRight();
13212 				redraw();
13213 			break;
13214 			case Key.Up:
13215 				textLayout.moveUp();
13216 				redraw();
13217 			break;
13218 			case Key.Down:
13219 				textLayout.moveDown();
13220 				redraw();
13221 			break;
13222 			case Key.Home:
13223 				textLayout.moveHome();
13224 				redraw();
13225 			break;
13226 			case Key.End:
13227 				textLayout.moveEnd();
13228 				redraw();
13229 			break;
13230 			case Key.PageUp:
13231 				foreach(i; 0 .. 32)
13232 				textLayout.moveUp();
13233 				redraw();
13234 			break;
13235 			case Key.PageDown:
13236 				foreach(i; 0 .. 32)
13237 				textLayout.moveDown();
13238 				redraw();
13239 			break;
13240 
13241 			default:
13242 				 {} // intentionally blank, let "char" handle it
13243 		}
13244 		/*
13245 		if(ev.key == Key.Backspace) {
13246 			textLayout.backspace();
13247 			redraw();
13248 		}
13249 		*/
13250 		ensureVisibleInScroll(textLayout.caretBoundingBox());
13251 	}
13252 
13253 	version(use_new_text_system) {
13254 		bool showingVerticalScroll() { return true; }
13255 		bool showingHorizontalScroll() { return true; }
13256 	}
13257 }
13258 
13259 ///
13260 class LineEdit : EditableTextWidget {
13261 	override bool showingVerticalScroll() { return false; }
13262 	override bool showingHorizontalScroll() { return false; }
13263 
13264 	 override int flexBasisWidth() { return 250; }
13265 	override int widthShrinkiness() { return 10; }
13266 
13267 	///
13268 	this(Widget parent) {
13269 		super(parent);
13270 		version(win32_widgets) {
13271 			createWin32Window(this, "edit"w, "",
13272 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13273 		} else version(custom_widgets) {
13274 			version(trash_text) {
13275 				setupCustomTextEditing();
13276 				addEventListener(delegate(CharEvent ev) {
13277 					if(ev.character == '\n')
13278 						ev.preventDefault();
13279 				});
13280 			}
13281 		} else static assert(false);
13282 	}
13283 
13284 	private this(bool useCustomWidget, Widget parent) {
13285 		if(!useCustomWidget)
13286 			this(parent);
13287 		else
13288 			super(true, parent);
13289 	}
13290 
13291 	version(use_new_text_system)
13292 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13293 		auto tdh = new TextDisplayHelper(textLayout, smw);
13294 		tdh.singleLine = true;
13295 		return tdh;
13296 	}
13297 
13298 	version(win32_widgets) {
13299 		mixin Padding!q{0};
13300 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13301 		override int maxHeight() { return minHeight; }
13302 	}
13303 
13304 	/+
13305 	@property void passwordMode(bool p) {
13306 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
13307 	}
13308 	+/
13309 }
13310 
13311 /// ditto
13312 class CustomLineEdit : LineEdit {
13313 	this(Widget parent) {
13314 		super(true, parent);
13315 	}
13316 }
13317 
13318 /++
13319 	A [LineEdit] that displays `*` in place of the actual characters.
13320 
13321 	Alas, Windows requires the window to be created differently to use this style,
13322 	so it had to be a new class instead of a toggle on and off on an existing object.
13323 
13324 	FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now.
13325 
13326 	History:
13327 		Added January 24, 2021
13328 +/
13329 class PasswordEdit : EditableTextWidget {
13330 	override bool showingVerticalScroll() { return false; }
13331 	override bool showingHorizontalScroll() { return false; }
13332 
13333 	override int flexBasisWidth() { return 250; }
13334 
13335 	version(use_new_text_system)
13336 	override TextStyle defaultTextStyle() {
13337 		auto cs = getComputedStyle();
13338 
13339 		auto osf = new class OperatingSystemFont {
13340 			this() {
13341 				super(cs.font);
13342 			}
13343 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
13344 				int count = 0;
13345 				foreach(dchar ch; text)
13346 					count++;
13347 				return count * super.stringWidth("*", window);
13348 			}
13349 		};
13350 
13351 		return new TextDisplayHelper.MyTextStyle(osf);
13352 	}
13353 
13354 	version(use_new_text_system)
13355 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13356 		static class TDH : TextDisplayHelper {
13357 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
13358 				singleLine = true;
13359 				super(textLayout, smw);
13360 			}
13361 
13362 			override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
13363 				char[256] buffer = void;
13364 				int bufferLength = 0;
13365 				foreach(dchar ch; text)
13366 					buffer[bufferLength++] = '*';
13367 				painter.drawText(upperLeft, buffer[0..bufferLength]);
13368 			}
13369 		}
13370 
13371 		return new TDH(textLayout, smw);
13372 	}
13373 
13374 	///
13375 	this(Widget parent) {
13376 		super(parent);
13377 		version(win32_widgets) {
13378 			createWin32Window(this, "edit"w, "",
13379 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
13380 		} else version(custom_widgets) {
13381 			version(trash_text) {
13382 				setupCustomTextEditing();
13383 
13384 				// should this be under trash text? i think so.
13385 				addEventListener(delegate(CharEvent ev) {
13386 					if(ev.character == '\n')
13387 						ev.preventDefault();
13388 				});
13389 			}
13390 		} else static assert(false);
13391 	}
13392 
13393 	private this(bool useCustomWidget, Widget parent) {
13394 		if(!useCustomWidget)
13395 			this(parent);
13396 		else
13397 			super(true, parent);
13398 	}
13399 
13400 	version(win32_widgets) {
13401 		mixin Padding!q{2};
13402 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
13403 		override int maxHeight() { return minHeight; }
13404 	}
13405 }
13406 
13407 /// ditto
13408 class CustomPasswordEdit : PasswordEdit {
13409 	this(Widget parent) {
13410 		super(true, parent);
13411 	}
13412 }
13413 
13414 
13415 ///
13416 class TextEdit : EditableTextWidget {
13417 	///
13418 	this(Widget parent) {
13419 		super(parent);
13420 		version(win32_widgets) {
13421 			createWin32Window(this, "edit"w, "",
13422 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
13423 		} else version(custom_widgets) {
13424 			version(trash_text)
13425 			setupCustomTextEditing();
13426 		} else static assert(false);
13427 	}
13428 
13429 	private this(bool useCustomWidget, Widget parent) {
13430 		if(!useCustomWidget)
13431 			this(parent);
13432 		else
13433 			super(true, parent);
13434 	}
13435 
13436 	override int maxHeight() { return int.max; }
13437 	override int heightStretchiness() { return 7; }
13438 
13439 	override int flexBasisWidth() { return 250; }
13440 	override int flexBasisHeight() { return 25; }
13441 }
13442 
13443 /// ditto
13444 class CustomTextEdit : TextEdit {
13445 	this(Widget parent) {
13446 		super(true, parent);
13447 	}
13448 }
13449 
13450 /+
13451 /++
13452 
13453 +/
13454 version(none)
13455 class RichTextDisplay : Widget {
13456 	@property void content(string c) {}
13457 	void appendContent(string c) {}
13458 }
13459 +/
13460 
13461 /++
13462 	A read-only text display
13463 
13464 	History:
13465 		Added October 31, 2023 (dub v11.3)
13466 +/
13467 class TextDisplay : EditableTextWidget {
13468 	this(string text, Widget parent) {
13469 		super(true, parent);
13470 		this.content = text;
13471 	}
13472 
13473 	override int maxHeight() { return int.max; }
13474 	override int minHeight() { return Window.defaultLineHeight; }
13475 	override int heightStretchiness() { return 7; }
13476 	override int heightShrinkiness() { return 2; }
13477 
13478 	override int flexBasisWidth() {
13479 		return scaleWithDpi(250);
13480 	}
13481 	override int flexBasisHeight() {
13482 		if(textLayout is null || this.tdh is null)
13483 			return Window.defaultLineHeight;
13484 
13485 		auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height;
13486 		return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height;
13487 	}
13488 
13489 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
13490 		return new MyTextDisplayHelper(textLayout, smw);
13491 	}
13492 
13493 	override void registerMovement() {
13494 		super.registerMovement();
13495 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
13496 	}
13497 
13498 	static class MyTextDisplayHelper : TextDisplayHelper {
13499 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
13500 			smw.verticalScrollBar.hide();
13501 			smw.horizontalScrollBar.hide();
13502 			super(textLayout, smw);
13503 			this.readonly = true;
13504 		}
13505 
13506 		override void registerMovement() {
13507 			super.registerMovement();
13508 
13509 			// FIXME: do the horizontal one too as needed and make sure that it does
13510 			// wordwrapping again
13511 			if(l.height + smw.horizontalScrollBar.height > this.height)
13512 				smw.verticalScrollBar.show();
13513 			else
13514 				smw.verticalScrollBar.hide();
13515 
13516 			l.wordWrapWidth = this.width;
13517 
13518 			smw.verticalScrollBar.setPosition = 0;
13519 		}
13520 	}
13521 
13522 	class Style : Widget.Style {
13523 		// just want the generic look for these
13524 	}
13525 
13526 	mixin OverrideStyle!Style;
13527 }
13528 
13529 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
13530 /++
13531 	A scrollable viewer for an array of widgets. The widgets inside a list item can be whatever you want, and you can have any number of total items you want because only the visible widgets need to actually exist and load their data at a time, giving constantly predictable performance.
13532 
13533 
13534 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
13535 
13536 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
13537 
13538 	Note that some state in reused widget objects may either be preserved or reset when the user isn't expecting it. It is your responsibility to handle this when you load an item (try to save it when it is unloaded, then set it when reloaded), but my recommendation would be to have minimal extra state. For example, avoid having a scrollable widget inside a list, since the scroll state might change as it goes out and into view. Instead, I'd suggest making the list be a loader for a details pane on the side.
13539 
13540 	History:
13541 		Added August 12, 2024 (dub v11.6)
13542 +/
13543 abstract class GenericListViewWidget : Widget {
13544 	/++
13545 
13546 	+/
13547 	this(Widget parent) {
13548 		super(parent);
13549 
13550 		smw = new ScrollMessageWidget(this);
13551 		smw.addDefaultKeyboardListeners();
13552 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
13553 
13554 		inner = new GenericListViewWidgetInner(this, smw);
13555 	}
13556 
13557 	private ScrollMessageWidget smw;
13558 	private GenericListViewWidgetInner inner;
13559 
13560 	/++
13561 
13562 	+/
13563 	abstract GenericListViewItem itemFactory(Widget parent);
13564 	// in device-dependent pixels
13565 	/++
13566 
13567 	+/
13568 	abstract Size itemSize(); // use 0 to indicate it can stretch?
13569 
13570 	enum LayoutMode {
13571 		rows,
13572 		columns,
13573 		gridRowsFirst,
13574 		gridColumnsFirst
13575 	}
13576 	LayoutMode layoutMode() {
13577 		return LayoutMode.rows;
13578 	}
13579 
13580 	private int itemCount_;
13581 
13582 	/++
13583 		Sets the count of available items in the list. This will not allocate any items, but it will adjust the scroll bars and try to load items up to this count on-demand as they appear visible.
13584 	+/
13585 	void setItemCount(int count) {
13586 		smw.setTotalArea(inner.width, count * itemSize().height);
13587 		smw.setViewableArea(inner.width, inner.height);
13588 		this.itemCount_ = count;
13589 	}
13590 
13591 	/++
13592 		Returns the current count of items expected to available in the list.
13593 	+/
13594 	int itemCount() {
13595 		return this.itemCount_;
13596 	}
13597 
13598 	/++
13599 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
13600 
13601 		Note you must $(I also) call [setItemCount] if the total item count has changed.
13602 	+/
13603 	void notifyItemsChanged(int index, int count = 1) {
13604 	}
13605 	/// ditto
13606 	void notifyItemsInserted(int index, int count = 1) {
13607 	}
13608 	/// ditto
13609 	void notifyItemsRemoved(int index, int count = 1) {
13610 	}
13611 	/// ditto
13612 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
13613 	}
13614 
13615 	private GenericListViewItem[] items;
13616 }
13617 
13618 /// ditto
13619 abstract class GenericListViewItem : Widget {
13620 	/++
13621 	+/
13622 	this(Widget parent) {
13623 		super(parent);
13624 	}
13625 
13626 	private int _currentIndex = -1;
13627 
13628 	private void showItemPrivate(int idx) {
13629 		showItem(idx);
13630 		_currentIndex = idx;
13631 	}
13632 
13633 	/++
13634 		Implement this to show an item from your data backing to the list.
13635 
13636 		Note that even if you are showing the requested index already, you should still try to reload it because it is possible the index now points to a different item (e.g. an item was added so all the indexes have changed) or if data has changed in this index and it is requesting you to update it prior to a repaint.
13637 	+/
13638 	abstract void showItem(int idx);
13639 
13640 	/++
13641 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
13642 
13643 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
13644 
13645 		Inside the call to `showItem`, `currentIndexLoaded` is the old index, and the argument to `showItem` is the new index. You might use that to save state to the right place as needed before you overwrite it with the new item.
13646 	+/
13647 	final int currentIndexLoaded() {
13648 		return _currentIndex;
13649 	}
13650 }
13651 
13652 ///
13653 unittest {
13654 	import arsd.minigui;
13655 
13656 	import std.conv;
13657 
13658 	void main() {
13659 		auto mw = new MainWindow();
13660 
13661 		static class MyListViewItem : GenericListViewItem {
13662 			this(Widget parent) {
13663 				super(parent);
13664 
13665 				label = new TextLabel("unloaded", TextAlignment.Left, this);
13666 				button = new Button("Click", this);
13667 
13668 				button.addEventListener("triggered", (){
13669 					messageBox(text("clicked ", currentIndexLoaded()));
13670 				});
13671 			}
13672 			override void showItem(int idx) {
13673 				label.label = "Item " ~ to!string(idx);
13674 			}
13675 
13676 			TextLabel label;
13677 			Button button;
13678 		}
13679 
13680 		auto widget = new class GenericListViewWidget {
13681 			this() {
13682 				super(mw);
13683 			}
13684 			override GenericListViewItem itemFactory(Widget parent) {
13685 				return new MyListViewItem(parent);
13686 			}
13687 			override Size itemSize() {
13688 				return Size(0, scaleWithDpi(80));
13689 			}
13690 		};
13691 
13692 		widget.setItemCount(5000);
13693 
13694 		mw.loop();
13695 	}
13696 }
13697 
13698 private class GenericListViewWidgetInner : Widget {
13699 	this(GenericListViewWidget glvw, ScrollMessageWidget smw) {
13700 		super(smw);
13701 		this.glvw = glvw;
13702 		this.tabStop = false;
13703 
13704 		reloadVisible();
13705 
13706 		smw.addEventListener("scroll", () {
13707 			reloadVisible();
13708 		});
13709 	}
13710 
13711 	override void registerMovement() {
13712 		super.registerMovement();
13713 		if(glvw && glvw.smw)
13714 			glvw.smw.setViewableArea(this.width, this.height);
13715 	}
13716 
13717 	void reloadVisible() {
13718 		auto y = glvw.smw.position.y / glvw.itemSize.height;
13719 		int offset = glvw.smw.position.y % glvw.itemSize.height;
13720 
13721 		if(offset || y >= glvw.itemCount())
13722 			y--;
13723 		if(y < 0)
13724 			y = 0;
13725 
13726 		recomputeChildLayout();
13727 
13728 		foreach(item; glvw.items) {
13729 			if(y < glvw.itemCount()) {
13730 				item.showItemPrivate(y);
13731 				item.show();
13732 			} else {
13733 				item.hide();
13734 			}
13735 			y++;
13736 		}
13737 
13738 		this.redraw();
13739 	}
13740 
13741 	private GenericListViewWidget glvw;
13742 
13743 	private bool inRcl;
13744 	override void recomputeChildLayout() {
13745 		if(inRcl)
13746 			return;
13747 		inRcl = true;
13748 		scope(exit)
13749 			inRcl = false;
13750 
13751 		auto ih = glvw.itemSize().height;
13752 
13753 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
13754 		bool hadNew;
13755 		while(glvw.items.length < itemCount) {
13756 			// FIXME: free the old items? maybe just set length
13757 			glvw.items ~= glvw.itemFactory(this);
13758 			hadNew = true;
13759 		}
13760 
13761 		if(hadNew)
13762 			reloadVisible();
13763 
13764 		int y = -(glvw.smw.position.y % ih);
13765 		foreach(child; children) {
13766 			child.x = 0;
13767 			child.y = y;
13768 			y += glvw.itemSize().height;
13769 			child.width = this.width;
13770 			child.height = ih;
13771 
13772 			child.recomputeChildLayout();
13773 		}
13774 	}
13775 }
13776 
13777 
13778 
13779 /++
13780 	History:
13781 		It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`.
13782 +/
13783 class MessageBox : Dialog {
13784 	private string message;
13785 	MessageBoxButton buttonPressed = MessageBoxButton.None;
13786 	/++
13787 
13788 		History:
13789 		The overload that takes `Window originator` was added on September 29, 2024.
13790 	+/
13791 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
13792 		this(null, message, buttons, buttonIds);
13793 	}
13794 	/// ditto
13795 	this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
13796 		message = message.stripRightInternal;
13797 		int mainWidth;
13798 
13799 		// estimate longest line
13800 		int count;
13801 		foreach(ch; message) {
13802 			if(ch == '\n') {
13803 				if(count > mainWidth)
13804 					mainWidth = count;
13805 				count = 0;
13806 			} else {
13807 				count++;
13808 			}
13809 		}
13810 		mainWidth *= 8;
13811 		if(mainWidth < 300)
13812 			mainWidth = 300;
13813 		if(mainWidth > 600)
13814 			mainWidth = 600;
13815 
13816 		super(originator, mainWidth, 100);
13817 
13818 		assert(buttons.length);
13819 		assert(buttons.length ==  buttonIds.length);
13820 
13821 		this.message = message;
13822 
13823 		auto label = new TextDisplay(message, this);
13824 
13825 		auto hl = new HorizontalLayout(this);
13826 		auto spacer = new HorizontalSpacer(hl); // to right align
13827 
13828 		foreach(idx, buttonText; buttons) {
13829 			auto button = new CommandButton(buttonText, hl);
13830 
13831 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
13832 				this.buttonPressed = buttonIds[idx];
13833 				win.close();
13834 			}; })(idx));
13835 
13836 			if(idx == 0)
13837 				button.focus();
13838 		}
13839 
13840 		if(buttons.length == 1)
13841 			auto spacer2 = new HorizontalSpacer(hl); // to center it
13842 
13843 		auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom;
13844 		auto max = scaleWithDpi(600); // random max height
13845 		if(size > max)
13846 			size = max;
13847 
13848 		win.resize(scaleWithDpi(mainWidth), size);
13849 
13850 		win.show();
13851 		redraw();
13852 	}
13853 
13854 	override void OK() {
13855 		this.win.close();
13856 	}
13857 
13858 	mixin Padding!q{16};
13859 }
13860 
13861 ///
13862 enum MessageBoxStyle {
13863 	OK, ///
13864 	OKCancel, ///
13865 	RetryCancel, ///
13866 	YesNo, ///
13867 	YesNoCancel, ///
13868 	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.
13869 }
13870 
13871 ///
13872 enum MessageBoxIcon {
13873 	None, ///
13874 	Info, ///
13875 	Warning, ///
13876 	Error ///
13877 }
13878 
13879 /// Identifies the button the user pressed on a message box.
13880 enum MessageBoxButton {
13881 	None, /// The user closed the message box without clicking any of the buttons.
13882 	OK, ///
13883 	Cancel, ///
13884 	Retry, ///
13885 	Yes, ///
13886 	No, ///
13887 	Continue ///
13888 }
13889 
13890 
13891 /++
13892 	Displays a modal message box, blocking until the user dismisses it.
13893 
13894 	Returns: the button pressed.
13895 +/
13896 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13897 	return messageBox(null, title, message, style, icon);
13898 }
13899 
13900 /// ditto
13901 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13902 	return messageBox(null, null, message, style, icon);
13903 }
13904 
13905 /++
13906 
13907 +/
13908 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13909 	version(win32_widgets) {
13910 		WCharzBuffer t = WCharzBuffer(title);
13911 		WCharzBuffer m = WCharzBuffer(message);
13912 		UINT type;
13913 		with(MessageBoxStyle)
13914 		final switch(style) {
13915 			case OK: type |= MB_OK; break;
13916 			case OKCancel: type |= MB_OKCANCEL; break;
13917 			case RetryCancel: type |= MB_RETRYCANCEL; break;
13918 			case YesNo: type |= MB_YESNO; break;
13919 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
13920 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
13921 		}
13922 		with(MessageBoxIcon)
13923 		final switch(icon) {
13924 			case None: break;
13925 			case Info: type |= MB_ICONINFORMATION; break;
13926 			case Warning: type |= MB_ICONWARNING; break;
13927 			case Error: type |= MB_ICONERROR; break;
13928 		}
13929 		switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) {
13930 			case IDOK: return MessageBoxButton.OK;
13931 			case IDCANCEL: return MessageBoxButton.Cancel;
13932 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
13933 			case IDYES: return MessageBoxButton.Yes;
13934 			case IDNO: return MessageBoxButton.No;
13935 			case IDCONTINUE: return MessageBoxButton.Continue;
13936 			default: return MessageBoxButton.None;
13937 		}
13938 	} else {
13939 		string[] buttons;
13940 		MessageBoxButton[] buttonIds;
13941 		with(MessageBoxStyle)
13942 		final switch(style) {
13943 			case OK:
13944 				buttons = ["OK"];
13945 				buttonIds = [MessageBoxButton.OK];
13946 			break;
13947 			case OKCancel:
13948 				buttons = ["OK", "Cancel"];
13949 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
13950 			break;
13951 			case RetryCancel:
13952 				buttons = ["Retry", "Cancel"];
13953 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
13954 			break;
13955 			case YesNo:
13956 				buttons = ["Yes", "No"];
13957 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
13958 			break;
13959 			case YesNoCancel:
13960 				buttons = ["Yes", "No", "Cancel"];
13961 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
13962 			break;
13963 			case RetryCancelContinue:
13964 				buttons = ["Try Again", "Cancel", "Continue"];
13965 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
13966 			break;
13967 		}
13968 		auto mb = new MessageBox(originator, message, buttons, buttonIds);
13969 		EventLoop el = EventLoop.get;
13970 		el.run(() { return !mb.win.closed; });
13971 		return mb.buttonPressed;
13972 	}
13973 
13974 }
13975 
13976 /// ditto
13977 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
13978 	return messageBox(originator, message, style, icon);
13979 }
13980 
13981 
13982 ///
13983 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
13984 
13985 /++
13986 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
13987 
13988 	History:
13989 		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.
13990 +/
13991 struct EventListener {
13992 	private Widget widget;
13993 	private string event;
13994 	private EventHandler handler;
13995 	private bool useCapture;
13996 
13997 	///
13998 	void disconnect() {
13999 		widget.removeEventListener(this);
14000 	}
14001 }
14002 
14003 /++
14004 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
14005 
14006 	Now, I recommend you use a statically typed event object instead.
14007 
14008 	See_Also: [Event]
14009 +/
14010 enum EventType : string {
14011 	click = "click", ///
14012 
14013 	mouseenter = "mouseenter", ///
14014 	mouseleave = "mouseleave", ///
14015 	mousein = "mousein", ///
14016 	mouseout = "mouseout", ///
14017 	mouseup = "mouseup", ///
14018 	mousedown = "mousedown", ///
14019 	mousemove = "mousemove", ///
14020 
14021 	keydown = "keydown", ///
14022 	keyup = "keyup", ///
14023 	char_ = "char", ///
14024 
14025 	focus = "focus", ///
14026 	blur = "blur", ///
14027 
14028 	triggered = "triggered", ///
14029 
14030 	change = "change", ///
14031 }
14032 
14033 /++
14034 	Represents an event that is currently being processed.
14035 
14036 
14037 	Minigui's event model is based on the web browser. An event has a name, a target,
14038 	and an associated data object. It starts from the window and works its way down through
14039 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
14040 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
14041 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
14042 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
14043 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
14044 	whenever propagation is done, not only if it gets to the end of the chain).
14045 
14046 	This model has several nice points:
14047 
14048 	$(LIST
14049 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
14050 		  with event handlers set, then add/remove children as much as you want without needing
14051 		  to manage the event handlers on them - the parent alone can manage everything.
14052 
14053 		* It is easy to create new custom events in your application.
14054 
14055 		* It is familiar to many web developers.
14056 	)
14057 
14058 	There's a few downsides though:
14059 
14060 	$(LIST
14061 		* There's not a lot of type safety.
14062 
14063 		* You don't get a static list of what events a widget can emit.
14064 
14065 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
14066 		  the central delegation benefit is it can be lead to debugging of action at a distance.
14067 	)
14068 
14069 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
14070 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
14071 	to simply use a D object type which provides a static interface as well as a built-in event name.
14072 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
14073 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
14074 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
14075 	to having a little more help from the D compiler and documentation generator.
14076 
14077 	Your code would change like this:
14078 
14079 	---
14080 	// old
14081 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
14082 
14083 	// new
14084 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
14085 	---
14086 
14087 	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.
14088 
14089 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
14090 
14091 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
14092 
14093 	Thus the family of functions are:
14094 
14095 	[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.
14096 
14097 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
14098 
14099 	[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.
14100 
14101 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
14102 
14103 	---
14104 	class MyCheckbox : Widget {
14105 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
14106 		/// It is NOT actually required but should be used whenever possible.
14107 		mixin Emits!(ChangeEvent!bool);
14108 
14109 		this(Widget parent) {
14110 			super(parent);
14111 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
14112 		}
14113 
14114 		private bool _checked;
14115 		@property bool checked() { return _checked; }
14116 		@property void checked(bool set) {
14117 			_checked = set;
14118 			emit!(ChangeEvent!bool)(&checked);
14119 		}
14120 	}
14121 	---
14122 
14123 	## Creating Your Own Events
14124 
14125 	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.
14126 
14127 	---
14128 	class MyEvent : Event {
14129 		this(Widget target) { super(EventString, target); }
14130 		mixin Register; // adds EventString and other reflection information
14131 	}
14132 	---
14133 
14134 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
14135 
14136 	History:
14137 		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.
14138 
14139 		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.
14140 +/
14141 /+
14142 
14143 	## General Conventions
14144 
14145 	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.
14146 
14147 
14148 	## Qt-style signals and slots
14149 
14150 	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.
14151 
14152 	The intention is for events to be used when
14153 
14154 	---
14155 	class Demo : Widget {
14156 		this() {
14157 			myPropertyChanged = Signal!int(this);
14158 		}
14159 		@property myProperty(int v) {
14160 			myPropertyChanged.emit(v);
14161 		}
14162 
14163 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
14164 		// but it can just genuinely not care about `this` since that's not really passed.
14165 	}
14166 
14167 	class Foo : Widget {
14168 		// the slot uda is not necessary, but it helps the script and ui builder find it.
14169 		@slot void setValue(int v) { ... }
14170 	}
14171 
14172 	demo.myPropertyChanged.connect(&foo.setValue);
14173 	---
14174 
14175 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
14176 
14177 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
14178 
14179 	class StringChangeEvent : ChangeEvent, Signal!string {
14180 		mixin SignalImpl
14181 	}
14182 
14183 +/
14184 class Event : ReflectableProperties {
14185 	/// Creates an event without populating any members and without sending it. See [dispatch]
14186 	this(string eventName, Widget emittedBy) {
14187 		this.eventName = eventName;
14188 		this.srcElement = emittedBy;
14189 	}
14190 
14191 
14192 	/// Implementations for the [ReflectableProperties] interface/
14193 	void getPropertiesList(scope void delegate(string name) sink) const {}
14194 	/// ditto
14195 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
14196 	/// ditto
14197 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
14198 		return SetPropertyResult.notPermitted;
14199 	}
14200 
14201 
14202 	/+
14203 	/++
14204 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
14205 
14206 		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.
14207 	+/
14208 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
14209 		if(value.length == 0) {
14210 			finalSink(memberName, `""`);
14211 			return;
14212 		}
14213 
14214 		char[1024] bufferBacking;
14215 		char[] buffer = bufferBacking;
14216 		int bufferPosition;
14217 
14218 		void sink(char ch) {
14219 			if(bufferPosition >= buffer.length)
14220 				buffer.length = buffer.length + 1024;
14221 			buffer[bufferPosition++] = ch;
14222 		}
14223 
14224 		sink('"');
14225 
14226 		foreach(ch; value) {
14227 			switch(ch) {
14228 				case '\\':
14229 					sink('\\'); sink('\\');
14230 				break;
14231 				case '"':
14232 					sink('\\'); sink('"');
14233 				break;
14234 				case '\n':
14235 					sink('\\'); sink('n');
14236 				break;
14237 				case '\r':
14238 					sink('\\'); sink('r');
14239 				break;
14240 				case '\t':
14241 					sink('\\'); sink('t');
14242 				break;
14243 				default:
14244 					sink(ch);
14245 			}
14246 		}
14247 
14248 		sink('"');
14249 
14250 		finalSink(memberName, buffer[0 .. bufferPosition]);
14251 	}
14252 	+/
14253 
14254 	/+
14255 	enum EventInitiator {
14256 		system,
14257 		minigui,
14258 		user
14259 	}
14260 
14261 	immutable EventInitiator; initiatedBy;
14262 	+/
14263 
14264 	/++
14265 		Events should generally follow the propagation model, but there's some exceptions
14266 		to that rule. If so, they should override this to return false. In that case, only
14267 		bubbling event handlers on the target itself and capturing event handlers on the containing
14268 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
14269 		capture -> target -> bubble process.)
14270 
14271 		History:
14272 			Added May 12, 2021
14273 	+/
14274 	bool propagates() const pure nothrow @nogc @safe {
14275 		return true;
14276 	}
14277 
14278 	/++
14279 		hints as to whether preventDefault will actually do anything. not entirely reliable.
14280 
14281 		History:
14282 			Added May 14, 2021
14283 	+/
14284 	bool cancelable() const pure nothrow @nogc @safe {
14285 		return true;
14286 	}
14287 
14288 	/++
14289 		You can mix this into child class to register some boilerplate. It includes the `EventString`
14290 		member, a constructor, and implementations of the dynamic get data interfaces.
14291 
14292 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
14293 
14294 
14295 		You can override the default EventString by simply providing your own in the form of
14296 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
14297 		which provides some namespace protection against conflicts in other libraries while still being fairly
14298 		easy to use.
14299 
14300 		If you provide your own constructor, it will override the default constructor provided here. A constructor
14301 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
14302 		first argument to your constructor.
14303 
14304 		History:
14305 			Added May 13, 2021.
14306 	+/
14307 	protected static mixin template Register() {
14308 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
14309 		this(Widget target) { super(EventString, target); }
14310 
14311 		mixin ReflectableProperties.RegisterGetters;
14312 	}
14313 
14314 	/++
14315 		This is the widget that emitted the event.
14316 
14317 
14318 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
14319 
14320 		History:
14321 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
14322 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
14323 			so I don't intend to remove these aliases.
14324 	+/
14325 	Widget source;
14326 	/// ditto
14327 	alias source target;
14328 	/// ditto
14329 	alias source srcElement;
14330 
14331 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
14332 
14333 	/// Prevents the default event handler (if there is one) from being called
14334 	void preventDefault() {
14335 		lastDefaultPrevented = true;
14336 		defaultPrevented = true;
14337 	}
14338 
14339 	/// Stops the event propagation immediately.
14340 	void stopPropagation() {
14341 		propagationStopped = true;
14342 	}
14343 
14344 	private bool defaultPrevented;
14345 	private bool propagationStopped;
14346 	private string eventName;
14347 
14348 	private bool isBubbling;
14349 
14350 	/// 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.
14351 	protected void adjustScrolling() { }
14352 	/// ditto
14353 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
14354 
14355 	/++
14356 		this sends it only to the target. If you want propagation, use dispatch() instead.
14357 
14358 		This should be made private!!!
14359 
14360 	+/
14361 	void sendDirectly() {
14362 		if(srcElement is null)
14363 			return;
14364 
14365 		// 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.
14366 
14367 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
14368 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
14369 
14370 		adjustScrolling();
14371 
14372 		if(auto e = target.parentWindow) {
14373 			if(auto handlers = "*" in e.capturingEventHandlers)
14374 			foreach(handler; *handlers)
14375 				if(handler) handler(e, this);
14376 			if(auto handlers = eventName in e.capturingEventHandlers)
14377 			foreach(handler; *handlers)
14378 				if(handler) handler(e, this);
14379 		}
14380 
14381 		auto e = srcElement;
14382 
14383 		if(auto handlers = eventName in e.bubblingEventHandlers)
14384 		foreach(handler; *handlers)
14385 			if(handler) handler(e, this);
14386 
14387 		if(auto handlers = "*" in e.bubblingEventHandlers)
14388 		foreach(handler; *handlers)
14389 			if(handler) handler(e, this);
14390 
14391 		// there's never a default for a catch-all event
14392 		if(!defaultPrevented)
14393 			if(eventName in e.defaultEventHandlers)
14394 				e.defaultEventHandlers[eventName](e, this);
14395 	}
14396 
14397 	/// this dispatches the element using the capture -> target -> bubble process
14398 	void dispatch() {
14399 		if(srcElement is null)
14400 			return;
14401 
14402 		if(!propagates) {
14403 			sendDirectly;
14404 			return;
14405 		}
14406 
14407 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
14408 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
14409 
14410 		adjustScrolling();
14411 		// first capture, then bubble
14412 
14413 		Widget[] chain;
14414 		Widget curr = srcElement;
14415 		while(curr) {
14416 			auto l = curr;
14417 			chain ~= l;
14418 			curr = curr.parent;
14419 		}
14420 
14421 		isBubbling = false;
14422 
14423 		foreach_reverse(e; chain) {
14424 			if(auto handlers = "*" in e.capturingEventHandlers)
14425 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14426 
14427 			if(propagationStopped)
14428 				break;
14429 
14430 			if(auto handlers = eventName in e.capturingEventHandlers)
14431 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14432 
14433 			// the default on capture should really be to always do nothing
14434 
14435 			//if(!defaultPrevented)
14436 			//	if(eventName in e.defaultEventHandlers)
14437 			//		e.defaultEventHandlers[eventName](e.element, this);
14438 
14439 			if(propagationStopped)
14440 				break;
14441 		}
14442 
14443 		int adjustX;
14444 		int adjustY;
14445 
14446 		isBubbling = true;
14447 		if(!propagationStopped)
14448 		foreach(e; chain) {
14449 			if(auto handlers = eventName in e.bubblingEventHandlers)
14450 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14451 
14452 			if(propagationStopped)
14453 				break;
14454 
14455 			if(auto handlers = "*" in e.bubblingEventHandlers)
14456 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
14457 
14458 			if(propagationStopped)
14459 				break;
14460 
14461 			if(e.encapsulatedChildren()) {
14462 				adjustClientCoordinates(adjustX, adjustY);
14463 				target = e;
14464 			} else {
14465 				adjustX += e.x;
14466 				adjustY += e.y;
14467 			}
14468 		}
14469 
14470 		if(!defaultPrevented)
14471 		foreach(e; chain) {
14472 			if(eventName in e.defaultEventHandlers)
14473 				e.defaultEventHandlers[eventName](e, this);
14474 		}
14475 	}
14476 
14477 
14478 	/* old compatibility things */
14479 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
14480 	final @property {
14481 		Key key() { return (cast(KeyEventBase) this).key; }
14482 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
14483 
14484 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
14485 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
14486 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
14487 	}
14488 
14489 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
14490 	final @property {
14491 		int clientX() { return (cast(MouseEventBase) this).clientX; }
14492 		int clientY() { return (cast(MouseEventBase) this).clientY; }
14493 
14494 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
14495 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
14496 
14497 		int button() { return (cast(MouseEventBase) this).button; }
14498 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
14499 	}
14500 
14501 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
14502 	final @property {
14503 		int state() {
14504 			if(auto meb = cast(MouseEventBase) this)
14505 				return meb.state;
14506 			if(auto keb = cast(KeyEventBase) this)
14507 				return keb.state;
14508 			assert(0);
14509 		}
14510 	}
14511 
14512 	deprecated("Use a CharEvent instead of Event in your handler going forward")
14513 	final @property {
14514 		dchar character() {
14515 			if(auto ce = cast(CharEvent) this)
14516 				return ce.character;
14517 			return dchar.init;
14518 		}
14519 	}
14520 
14521 	// for change events
14522 	@property {
14523 		///
14524 		int intValue() { return 0; }
14525 		///
14526 		string stringValue() { return null; }
14527 	}
14528 }
14529 
14530 /++
14531 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
14532 
14533 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
14534 	dynamic and custom events, but the static list helps ensure you get them right.
14535 
14536 	If this is declared, you can use [Widget.emit] to send the event.
14537 
14538 	All events work the same way though, following the capture->widget->bubble model described under [Event].
14539 
14540 	History:
14541 		Added May 4, 2021
14542 +/
14543 mixin template Emits(EventType) {
14544 	import arsd.minigui : EventString;
14545 	static if(is(EventType : Event) && !is(EventType == Event))
14546 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
14547 	else
14548 		static assert(0, "You can only emit subclasses of Event");
14549 }
14550 
14551 /// ditto
14552 mixin template Emits(string eventString) {
14553 	mixin("private Event[0] emits_" ~ eventString ~";");
14554 }
14555 
14556 /*
14557 class SignalEvent(string name) : Event {
14558 
14559 }
14560 */
14561 
14562 /++
14563 	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".
14564 
14565 
14566 	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.
14567 
14568 	History:
14569 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
14570 +/
14571 class CommandEvent : Event {
14572 	enum EventString = "command";
14573 	this(Widget source, string CommandString = EventString) {
14574 		super(CommandString, source);
14575 	}
14576 }
14577 
14578 /++
14579 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
14580 +/
14581 class CommandEventWithArgs(Args...) : CommandEvent {
14582 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
14583 	Args args;
14584 }
14585 
14586 /++
14587 	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.
14588 
14589 	See [CommandEvent] for more information.
14590 
14591 	Returns:
14592 		The [EventListener] you can use to remove the handler.
14593 +/
14594 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
14595 	return w.addEventListener(CommandString, (Event ev) {
14596 		if(ev.target is w)
14597 			return; // it does not consume its own commands!
14598 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
14599 			handler(cev.args);
14600 			ev.stopPropagation();
14601 		}
14602 	});
14603 }
14604 
14605 /++
14606 	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.
14607 +/
14608 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
14609 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
14610 	event.dispatch();
14611 }
14612 
14613 class ResizeEvent : Event {
14614 	enum EventString = "resize";
14615 
14616 	this(Widget target) { super(EventString, target); }
14617 
14618 	override bool propagates() const { return false; }
14619 }
14620 
14621 /++
14622 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
14623 
14624 	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.
14625 
14626 	History:
14627 		Added June 21, 2021 (dub v10.1)
14628 +/
14629 class ClosingEvent : Event {
14630 	enum EventString = "closing";
14631 
14632 	this(Widget target) { super(EventString, target); }
14633 
14634 	override bool propagates() const { return false; }
14635 	override bool cancelable() const { return true; }
14636 }
14637 
14638 /// ditto
14639 class ClosedEvent : Event {
14640 	enum EventString = "closed";
14641 
14642 	this(Widget target) { super(EventString, target); }
14643 
14644 	override bool propagates() const { return false; }
14645 	override bool cancelable() const { return false; }
14646 }
14647 
14648 ///
14649 class BlurEvent : Event {
14650 	enum EventString = "blur";
14651 
14652 	// FIXME: related target?
14653 	this(Widget target) { super(EventString, target); }
14654 
14655 	override bool propagates() const { return false; }
14656 }
14657 
14658 ///
14659 class FocusEvent : Event {
14660 	enum EventString = "focus";
14661 
14662 	// FIXME: related target?
14663 	this(Widget target) { super(EventString, target); }
14664 
14665 	override bool propagates() const { return false; }
14666 }
14667 
14668 /++
14669 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
14670 
14671 	History:
14672 		Added July 3, 2021
14673 +/
14674 class FocusInEvent : Event {
14675 	enum EventString = "focusin";
14676 
14677 	// FIXME: related target?
14678 	this(Widget target) { super(EventString, target); }
14679 
14680 	override bool cancelable() const { return false; }
14681 }
14682 
14683 /// ditto
14684 class FocusOutEvent : Event {
14685 	enum EventString = "focusout";
14686 
14687 	// FIXME: related target?
14688 	this(Widget target) { super(EventString, target); }
14689 
14690 	override bool cancelable() const { return false; }
14691 }
14692 
14693 ///
14694 class ScrollEvent : Event {
14695 	enum EventString = "scroll";
14696 	this(Widget target) { super(EventString, target); }
14697 
14698 	override bool cancelable() const { return false; }
14699 }
14700 
14701 /++
14702 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
14703 
14704 	History:
14705 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
14706 +/
14707 class CharEvent : Event {
14708 	enum EventString = "char";
14709 	this(Widget target, dchar ch) {
14710 		character = ch;
14711 		super(EventString, target);
14712 	}
14713 
14714 	immutable dchar character;
14715 }
14716 
14717 /++
14718 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
14719 +/
14720 abstract class ChangeEventBase : Event {
14721 	enum EventString = "change";
14722 	this(Widget target) {
14723 		super(EventString, target);
14724 	}
14725 
14726 	/+
14727 		// idk where or how exactly i want to do this.
14728 		// i might come back to it later.
14729 
14730 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
14731 	// this way the source doesn't get too confused (think of a nested scroll widget)
14732 	//
14733 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
14734 	// then you consume that command and change you scroll x position to whatever. then you do
14735 	// some kind of change event that is broadcast back to the children and any horizontal scroll
14736 	// listeners are now able to update, without having an explicit connection between them.
14737 	void broadcastToChildren(string fieldName) {
14738 
14739 	}
14740 	+/
14741 }
14742 
14743 /++
14744 	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.
14745 
14746 
14747 	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).
14748 
14749 	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);`
14750 
14751 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
14752 
14753 	History:
14754 		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.
14755 +/
14756 class ChangeEvent(T) : ChangeEventBase {
14757 	this(Widget target, T delegate() getNewValue) {
14758 		assert(getNewValue !is null);
14759 		this.getNewValue = getNewValue;
14760 		super(target);
14761 	}
14762 
14763 	private T delegate() getNewValue;
14764 
14765 	/++
14766 		Gets the new value that just changed.
14767 	+/
14768 	@property T value() {
14769 		return getNewValue();
14770 	}
14771 
14772 	/// compatibility method for old generic Events
14773 	static if(is(immutable T == immutable int))
14774 		override int intValue() { return value; }
14775 	/// ditto
14776 	static if(is(immutable T == immutable string))
14777 		override string stringValue() { return value; }
14778 }
14779 
14780 /++
14781 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
14782 
14783 
14784 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14785 
14786 	History:
14787 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14788 +/
14789 abstract class KeyEventBase : Event {
14790 	this(string name, Widget target) {
14791 		super(name, target);
14792 	}
14793 
14794 	// for key events
14795 	Key key; ///
14796 
14797 	KeyEvent originalKeyEvent;
14798 
14799 	/++
14800 		Indicates the current state of the given keyboard modifier keys.
14801 
14802 		History:
14803 			Added to events on April 15, 2020.
14804 	+/
14805 	bool ctrlKey;
14806 
14807 	/// ditto
14808 	bool altKey;
14809 
14810 	/// ditto
14811 	bool shiftKey;
14812 
14813 	/++
14814 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
14815 
14816 		See [arsd.simpledisplay.ModifierState] for other possible flags.
14817 	+/
14818 	int state;
14819 
14820 	mixin Register;
14821 }
14822 
14823 /++
14824 	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].
14825 
14826 
14827 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14828 
14829 	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.
14830 
14831 	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.
14832 
14833 	See_Also: [KeyUpEvent], [CharEvent]
14834 
14835 	History:
14836 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
14837 +/
14838 class KeyDownEvent : KeyEventBase {
14839 	enum EventString = "keydown";
14840 	this(Widget target) { super(EventString, target); }
14841 }
14842 
14843 /++
14844 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
14845 
14846 
14847 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14848 
14849 	See_Also: [KeyDownEvent], [CharEvent]
14850 
14851 	History:
14852 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
14853 +/
14854 class KeyUpEvent : KeyEventBase {
14855 	enum EventString = "keyup";
14856 	this(Widget target) { super(EventString, target); }
14857 }
14858 
14859 /++
14860 	Contains shared properties for various mouse events;
14861 
14862 
14863 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14864 
14865 	History:
14866 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
14867 +/
14868 abstract class MouseEventBase : Event {
14869 	this(string name, Widget target) {
14870 		super(name, target);
14871 	}
14872 
14873 	// for mouse events
14874 	int clientX; /// The mouse event location relative to the target widget
14875 	int clientY; /// ditto
14876 
14877 	int viewportX; /// The mouse event location relative to the window origin
14878 	int viewportY; /// ditto
14879 
14880 	int button; /// See: [MouseEvent.button]
14881 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
14882 
14883 	/++
14884 		Indicates the current state of the given keyboard modifier keys.
14885 
14886 		History:
14887 			Added to mouse events on September 28, 2010.
14888 	+/
14889 	bool ctrlKey;
14890 
14891 	/// ditto
14892 	bool altKey;
14893 
14894 	/// ditto
14895 	bool shiftKey;
14896 
14897 
14898 
14899 	int state; ///
14900 
14901 	/++
14902 		for consistent names with key event.
14903 
14904 		History:
14905 			Added September 28, 2021 (dub v10.3)
14906 	+/
14907 	alias modifierState = state;
14908 
14909 	/++
14910 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
14911 
14912 		History:
14913 			Added May 15, 2021
14914 	+/
14915 	bool isMouseWheel() {
14916 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
14917 	}
14918 
14919 	// private
14920 	override void adjustClientCoordinates(int deltaX, int deltaY) {
14921 		clientX += deltaX;
14922 		clientY += deltaY;
14923 	}
14924 
14925 	override void adjustScrolling() {
14926 	version(custom_widgets) { // TEMP
14927 		viewportX = clientX;
14928 		viewportY = clientY;
14929 		if(auto se = cast(ScrollableWidget) srcElement) {
14930 			clientX += se.scrollOrigin.x;
14931 			clientY += se.scrollOrigin.y;
14932 		} else if(auto se = cast(ScrollableContainerWidget) srcElement) {
14933 			//clientX += se.scrollX_;
14934 			//clientY += se.scrollY_;
14935 		}
14936 	}
14937 	}
14938 
14939 	mixin Register;
14940 }
14941 
14942 /++
14943 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
14944 
14945 
14946 	$(WARNING
14947 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
14948 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
14949 		behavior.
14950 	)
14951 
14952 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
14953 
14954 	[MouseUpEvent] is sent when the user releases a mouse button.
14955 
14956 	[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.)
14957 
14958 	[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.
14959 
14960 	[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.
14961 
14962 	[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.
14963 
14964 	[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.
14965 
14966 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
14967 
14968 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
14969 
14970 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
14971 
14972 	Rationale:
14973 
14974 		If you only want to do drag, mousedown/up works just fine being consistently sent.
14975 
14976 		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).
14977 
14978 		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.
14979 
14980 	History:
14981 		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.
14982 +/
14983 class MouseUpEvent : MouseEventBase {
14984 	enum EventString = "mouseup"; ///
14985 	this(Widget target) { super(EventString, target); }
14986 }
14987 /// ditto
14988 class MouseDownEvent : MouseEventBase {
14989 	enum EventString = "mousedown"; ///
14990 	this(Widget target) { super(EventString, target); }
14991 }
14992 /// ditto
14993 class MouseMoveEvent : MouseEventBase {
14994 	enum EventString = "mousemove"; ///
14995 	this(Widget target) { super(EventString, target); }
14996 }
14997 /// ditto
14998 class ClickEvent : MouseEventBase {
14999 	enum EventString = "click"; ///
15000 	this(Widget target) { super(EventString, target); }
15001 }
15002 /// ditto
15003 class DoubleClickEvent : MouseEventBase {
15004 	enum EventString = "dblclick"; ///
15005 	this(Widget target) { super(EventString, target); }
15006 }
15007 /// ditto
15008 class MouseOverEvent : Event {
15009 	enum EventString = "mouseover"; ///
15010 	this(Widget target) { super(EventString, target); }
15011 }
15012 /// ditto
15013 class MouseOutEvent : Event {
15014 	enum EventString = "mouseout"; ///
15015 	this(Widget target) { super(EventString, target); }
15016 }
15017 /// ditto
15018 class MouseEnterEvent : Event {
15019 	enum EventString = "mouseenter"; ///
15020 	this(Widget target) { super(EventString, target); }
15021 
15022 	override bool propagates() const { return false; }
15023 }
15024 /// ditto
15025 class MouseLeaveEvent : Event {
15026 	enum EventString = "mouseleave"; ///
15027 	this(Widget target) { super(EventString, target); }
15028 
15029 	override bool propagates() const { return false; }
15030 }
15031 
15032 private bool isAParentOf(Widget a, Widget b) {
15033 	if(a is null || b is null)
15034 		return false;
15035 
15036 	while(b !is null) {
15037 		if(a is b)
15038 			return true;
15039 		b = b.parent;
15040 	}
15041 
15042 	return false;
15043 }
15044 
15045 private struct WidgetAtPointResponse {
15046 	Widget widget;
15047 
15048 	// x, y relative to the widget in the response.
15049 	int x;
15050 	int y;
15051 }
15052 
15053 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
15054 	assert(starting !is null);
15055 
15056 	starting.addScrollPosition(x, y);
15057 
15058 	auto child = starting.getChildAtPosition(x, y);
15059 	while(child) {
15060 		if(child.hidden)
15061 			continue;
15062 		starting = child;
15063 		x -= child.x;
15064 		y -= child.y;
15065 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
15066 		child = r.widget;
15067 		if(child is starting)
15068 			break;
15069 	}
15070 	return WidgetAtPointResponse(starting, x, y);
15071 }
15072 
15073 version(win32_widgets) {
15074 private:
15075 	import core.sys.windows.commctrl;
15076 
15077 	pragma(lib, "comctl32");
15078 	shared static this() {
15079 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
15080 		INITCOMMONCONTROLSEX ic;
15081 		ic.dwSize = cast(DWORD) ic.sizeof;
15082 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
15083 		if(!InitCommonControlsEx(&ic)) {
15084 			//writeln("ICC failed");
15085 		}
15086 	}
15087 
15088 
15089 	// everything from here is just win32 headers copy pasta
15090 private:
15091 extern(Windows):
15092 
15093 	alias HANDLE HMENU;
15094 	HMENU CreateMenu();
15095 	bool SetMenu(HWND, HMENU);
15096 	HMENU CreatePopupMenu();
15097 	enum MF_POPUP = 0x10;
15098 	enum MF_STRING = 0;
15099 
15100 
15101 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
15102 	struct INITCOMMONCONTROLSEX {
15103 		DWORD dwSize;
15104 		DWORD dwICC;
15105 	}
15106 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
15107 enum {
15108         IDB_STD_SMALL_COLOR,
15109         IDB_STD_LARGE_COLOR,
15110         IDB_VIEW_SMALL_COLOR = 4,
15111         IDB_VIEW_LARGE_COLOR = 5
15112 }
15113 enum {
15114         STD_CUT,
15115         STD_COPY,
15116         STD_PASTE,
15117         STD_UNDO,
15118         STD_REDOW,
15119         STD_DELETE,
15120         STD_FILENEW,
15121         STD_FILEOPEN,
15122         STD_FILESAVE,
15123         STD_PRINTPRE,
15124         STD_PROPERTIES,
15125         STD_HELP,
15126         STD_FIND,
15127         STD_REPLACE,
15128         STD_PRINT // = 14
15129 }
15130 
15131 alias HANDLE HIMAGELIST;
15132 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
15133 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
15134         BOOL ImageList_Destroy(HIMAGELIST);
15135 
15136 uint MAKELONG(ushort a, ushort b) {
15137         return cast(uint) ((b << 16) | a);
15138 }
15139 
15140 
15141 struct TBBUTTON {
15142 	int   iBitmap;
15143 	int   idCommand;
15144 	BYTE  fsState;
15145 	BYTE  fsStyle;
15146 	version(Win64)
15147 	BYTE[6] bReserved;
15148 	else
15149 	BYTE[2]  bReserved;
15150 	DWORD dwData;
15151 	INT_PTR   iString;
15152 }
15153 
15154 	enum {
15155 		TB_ADDBUTTONSA   = WM_USER + 20,
15156 		TB_INSERTBUTTONA = WM_USER + 21,
15157 		TB_GETIDEALSIZE = WM_USER + 99,
15158 	}
15159 
15160 struct SIZE {
15161 	LONG cx;
15162 	LONG cy;
15163 }
15164 
15165 
15166 enum {
15167 	TBSTATE_CHECKED       = 1,
15168 	TBSTATE_PRESSED       = 2,
15169 	TBSTATE_ENABLED       = 4,
15170 	TBSTATE_HIDDEN        = 8,
15171 	TBSTATE_INDETERMINATE = 16,
15172 	TBSTATE_WRAP          = 32
15173 }
15174 
15175 
15176 
15177 enum {
15178 	ILC_COLOR    = 0,
15179 	ILC_COLOR4   = 4,
15180 	ILC_COLOR8   = 8,
15181 	ILC_COLOR16  = 16,
15182 	ILC_COLOR24  = 24,
15183 	ILC_COLOR32  = 32,
15184 	ILC_COLORDDB = 254,
15185 	ILC_MASK     = 1,
15186 	ILC_PALETTE  = 2048
15187 }
15188 
15189 
15190 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
15191 
15192 
15193 enum {
15194 	TB_ENABLEBUTTON          = WM_USER + 1,
15195 	TB_CHECKBUTTON,
15196 	TB_PRESSBUTTON,
15197 	TB_HIDEBUTTON,
15198 	TB_INDETERMINATE, //     = WM_USER + 5,
15199 	TB_ISBUTTONENABLED       = WM_USER + 9,
15200 	TB_ISBUTTONCHECKED,
15201 	TB_ISBUTTONPRESSED,
15202 	TB_ISBUTTONHIDDEN,
15203 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
15204 	TB_SETSTATE              = WM_USER + 17,
15205 	TB_GETSTATE              = WM_USER + 18,
15206 	TB_ADDBITMAP             = WM_USER + 19,
15207 	TB_DELETEBUTTON          = WM_USER + 22,
15208 	TB_GETBUTTON,
15209 	TB_BUTTONCOUNT,
15210 	TB_COMMANDTOINDEX,
15211 	TB_SAVERESTOREA,
15212 	TB_CUSTOMIZE,
15213 	TB_ADDSTRINGA,
15214 	TB_GETITEMRECT,
15215 	TB_BUTTONSTRUCTSIZE,
15216 	TB_SETBUTTONSIZE,
15217 	TB_SETBITMAPSIZE,
15218 	TB_AUTOSIZE, //          = WM_USER + 33,
15219 	TB_GETTOOLTIPS           = WM_USER + 35,
15220 	TB_SETTOOLTIPS           = WM_USER + 36,
15221 	TB_SETPARENT             = WM_USER + 37,
15222 	TB_SETROWS               = WM_USER + 39,
15223 	TB_GETROWS,
15224 	TB_GETBITMAPFLAGS,
15225 	TB_SETCMDID,
15226 	TB_CHANGEBITMAP,
15227 	TB_GETBITMAP,
15228 	TB_GETBUTTONTEXTA,
15229 	TB_REPLACEBITMAP, //     = WM_USER + 46,
15230 	TB_GETBUTTONSIZE         = WM_USER + 58,
15231 	TB_SETBUTTONWIDTH        = WM_USER + 59,
15232 	TB_GETBUTTONTEXTW        = WM_USER + 75,
15233 	TB_SAVERESTOREW          = WM_USER + 76,
15234 	TB_ADDSTRINGW            = WM_USER + 77,
15235 }
15236 
15237 extern(Windows)
15238 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
15239 
15240 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
15241 
15242 
15243 	enum {
15244 		TB_SETINDENT = WM_USER + 47,
15245 		TB_SETIMAGELIST,
15246 		TB_GETIMAGELIST,
15247 		TB_LOADIMAGES,
15248 		TB_GETRECT,
15249 		TB_SETHOTIMAGELIST,
15250 		TB_GETHOTIMAGELIST,
15251 		TB_SETDISABLEDIMAGELIST,
15252 		TB_GETDISABLEDIMAGELIST,
15253 		TB_SETSTYLE,
15254 		TB_GETSTYLE,
15255 		//TB_GETBUTTONSIZE,
15256 		//TB_SETBUTTONWIDTH,
15257 		TB_SETMAXTEXTROWS,
15258 		TB_GETTEXTROWS // = WM_USER + 61
15259 	}
15260 
15261 enum {
15262 	CCM_FIRST            = 0x2000,
15263 	CCM_LAST             = CCM_FIRST + 0x200,
15264 	CCM_SETBKCOLOR       = 8193,
15265 	CCM_SETCOLORSCHEME   = 8194,
15266 	CCM_GETCOLORSCHEME   = 8195,
15267 	CCM_GETDROPTARGET    = 8196,
15268 	CCM_SETUNICODEFORMAT = 8197,
15269 	CCM_GETUNICODEFORMAT = 8198,
15270 	CCM_SETVERSION       = 0x2007,
15271 	CCM_GETVERSION       = 0x2008,
15272 	CCM_SETNOTIFYWINDOW  = 0x2009
15273 }
15274 
15275 
15276 enum {
15277 	PBM_SETRANGE     = WM_USER + 1,
15278 	PBM_SETPOS,
15279 	PBM_DELTAPOS,
15280 	PBM_SETSTEP,
15281 	PBM_STEPIT,   // = WM_USER + 5
15282 	PBM_SETRANGE32   = 1030,
15283 	PBM_GETRANGE,
15284 	PBM_GETPOS,
15285 	PBM_SETBARCOLOR, // = 1033
15286 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
15287 }
15288 
15289 enum {
15290 	PBS_SMOOTH   = 1,
15291 	PBS_VERTICAL = 4
15292 }
15293 
15294 enum {
15295         ICC_LISTVIEW_CLASSES = 1,
15296         ICC_TREEVIEW_CLASSES = 2,
15297         ICC_BAR_CLASSES      = 4,
15298         ICC_TAB_CLASSES      = 8,
15299         ICC_UPDOWN_CLASS     = 16,
15300         ICC_PROGRESS_CLASS   = 32,
15301         ICC_HOTKEY_CLASS     = 64,
15302         ICC_ANIMATE_CLASS    = 128,
15303         ICC_WIN95_CLASSES    = 255,
15304         ICC_DATE_CLASSES     = 256,
15305         ICC_USEREX_CLASSES   = 512,
15306         ICC_COOL_CLASSES     = 1024,
15307 	ICC_STANDARD_CLASSES = 0x00004000,
15308 }
15309 
15310 	enum WM_USER = 1024;
15311 }
15312 
15313 version(win32_widgets)
15314 	pragma(lib, "comdlg32");
15315 
15316 
15317 ///
15318 enum GenericIcons : ushort {
15319 	None, ///
15320 	// these happen to match the win32 std icons numerically if you just subtract one from the value
15321 	Cut, ///
15322 	Copy, ///
15323 	Paste, ///
15324 	Undo, ///
15325 	Redo, ///
15326 	Delete, ///
15327 	New, ///
15328 	Open, ///
15329 	Save, ///
15330 	PrintPreview, ///
15331 	Properties, ///
15332 	Help, ///
15333 	Find, ///
15334 	Replace, ///
15335 	Print, ///
15336 }
15337 
15338 enum FileDialogType {
15339 	Automatic,
15340 	Open,
15341 	Save
15342 }
15343 string previousFileReferenced;
15344 
15345 /++
15346 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
15347 
15348 	Params:
15349 		storage = an alias to a `static string` variable that stores the last file referenced. It will
15350 		use this to pre-fill the dialog with a suggestion.
15351 
15352 		Please note that it MUST be `static` or you will get compile errors.
15353 
15354 		filters = the filters param to [getFileName]
15355 
15356 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
15357 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
15358 		a save dialog box. Otherwise, it will show an open dialog box.
15359 +/
15360 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
15361 	string name;
15362 	alias name this;
15363 }
15364 
15365 /++
15366 	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.
15367 
15368 	History:
15369 		onCancel was added November 6, 2021.
15370 
15371 		The dialog itself on Linux was modified on December 2, 2021 to include
15372 		a directory picker in addition to the command line completion view.
15373 
15374 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
15375 
15376 		The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version.
15377 	Future_directions:
15378 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
15379 		at least on Linux, maybe on Windows too.
15380 +/
15381 void getOpenFileName(
15382 	Window owner,
15383 	void delegate(string) onOK,
15384 	string prefilledName = null,
15385 	string[] filters = null,
15386 	void delegate() onCancel = null,
15387 	string initialDirectory = null,
15388 )
15389 {
15390 	return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory);
15391 }
15392 
15393 /// ditto
15394 void getSaveFileName(
15395 	Window owner,
15396 	void delegate(string) onOK,
15397 	string prefilledName = null,
15398 	string[] filters = null,
15399 	void delegate() onCancel = null,
15400 	string initialDirectory = null,
15401 )
15402 {
15403 	return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory);
15404 }
15405 
15406 // deprecated("Pass an explicit owner window as the first argument, even if `null`. You can usually pass the `parentWindow` member of the widget that prompted this interaction.")
15407 /// ditto
15408 void getOpenFileName(
15409 	void delegate(string) onOK,
15410 	string prefilledName = null,
15411 	string[] filters = null,
15412 	void delegate() onCancel = null,
15413 	string initialDirectory = null,
15414 )
15415 {
15416 	return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory);
15417 }
15418 
15419 /// ditto
15420 void getSaveFileName(
15421 	void delegate(string) onOK,
15422 	string prefilledName = null,
15423 	string[] filters = null,
15424 	void delegate() onCancel = null,
15425 	string initialDirectory = null,
15426 )
15427 {
15428 	return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory);
15429 }
15430 
15431 void getFileName(
15432 	Window owner,
15433 	bool openOrSave,
15434 	void delegate(string) onOK,
15435 	string prefilledName = null,
15436 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
15437 	void delegate() onCancel = null,
15438 	string initialDirectory = null,
15439 )
15440 {
15441 
15442 	version(win32_widgets) {
15443 		import core.sys.windows.commdlg;
15444 	/*
15445 	Ofn.lStructSize = sizeof(OPENFILENAME);
15446 	Ofn.hwndOwner = hWnd;
15447 	Ofn.lpstrFilter = szFilter;
15448 	Ofn.lpstrFile= szFile;
15449 	Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
15450 	Ofn.lpstrFileTitle = szFileTitle;
15451 	Ofn.nMaxFileTitle = sizeof(szFileTitle);
15452 	Ofn.lpstrInitialDir = (LPSTR)NULL;
15453 	Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
15454 	Ofn.lpstrTitle = szTitle;
15455 	 */
15456 
15457 
15458 		wchar[1024] file = 0;
15459 		wchar[1024] filterBuffer = 0;
15460 		makeWindowsString(prefilledName, file[]);
15461 		OPENFILENAME ofn;
15462 		ofn.lStructSize = ofn.sizeof;
15463 		ofn.hwndOwner = owner is null ? null : owner.win.hwnd;
15464 		if(filters.length) {
15465 			string filter;
15466 			foreach(i, f; filters) {
15467 				filter ~= f;
15468 				filter ~= "\0";
15469 			}
15470 			filter ~= "\0";
15471 			ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
15472 		}
15473 		ofn.lpstrFile = file.ptr;
15474 		ofn.nMaxFile = file.length;
15475 
15476 		wchar[1024] initialDir = 0;
15477 		if(initialDirectory !is null) {
15478 			makeWindowsString(initialDirectory, initialDir[]);
15479 			ofn.lpstrInitialDir = file.ptr;
15480 		}
15481 
15482 		if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
15483 		{
15484 			string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
15485 			if(okString.length && okString[$-1] == '\0')
15486 				okString = okString[0..$-1];
15487 			onOK(okString);
15488 		} else {
15489 			if(onCancel)
15490 				onCancel();
15491 		}
15492 	} else version(custom_widgets) {
15493 		if(filters.length == 0)
15494 			filters = ["All Files\0*.*"];
15495 		auto picker = new FilePicker(prefilledName, filters, initialDirectory, owner);
15496 		picker.onOK = onOK;
15497 		picker.onCancel = onCancel;
15498 		picker.show();
15499 	}
15500 }
15501 
15502 version(custom_widgets)
15503 private
15504 class FilePicker : Dialog {
15505 	void delegate(string) onOK;
15506 	void delegate() onCancel;
15507 	LineEdit lineEdit;
15508 
15509 	// returns common prefix
15510 	string loadFiles(string cwd, string[] filters...) {
15511 		string[] files;
15512 		string[] dirs;
15513 
15514 		string commonPrefix;
15515 
15516 		getFiles(cwd, (string name, bool isDirectory) {
15517 			if(name == ".")
15518 				return; // skip this as unnecessary
15519 			if(isDirectory)
15520 				dirs ~= name;
15521 			else {
15522 				foreach(filter; filters)
15523 				if(
15524 					filter.length <= 1 ||
15525 					filter == "*.*" ||
15526 					(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
15527 					(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
15528 				)
15529 				{
15530 					files ~= name;
15531 
15532 					if(filter.length > 0 && filter[$-1] == '*') {
15533 						if(commonPrefix is null) {
15534 							commonPrefix = name;
15535 						} else {
15536 							foreach(idx, char i; name) {
15537 								if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
15538 									commonPrefix = commonPrefix[0 .. idx];
15539 									break;
15540 								}
15541 							}
15542 						}
15543 					}
15544 
15545 					break;
15546 				}
15547 			}
15548 		});
15549 
15550 		extern(C) static int comparator(scope const void* a, scope const void* b) {
15551 			// FIXME: make it a natural sort for numbers
15552 			// maybe put dot files at the end too.
15553 			auto sa = *cast(string*) a;
15554 			auto sb = *cast(string*) b;
15555 
15556 			for(int i = 0; i < sa.length; i++) {
15557 				if(i == sb.length)
15558 					return 1;
15559 				auto diff = sa[i] - sb[i];
15560 				if(diff)
15561 					return diff;
15562 			}
15563 
15564 			return 0;
15565 		}
15566 
15567 		nonPhobosSort(files, &comparator);
15568 		nonPhobosSort(dirs, &comparator);
15569 
15570 		listWidget.clear();
15571 		dirWidget.clear();
15572 		foreach(name; dirs)
15573 			dirWidget.addOption(name);
15574 		foreach(name; files)
15575 			listWidget.addOption(name);
15576 
15577 		return commonPrefix;
15578 	}
15579 
15580 	ListWidget listWidget;
15581 	ListWidget dirWidget;
15582 
15583 	string currentDirectory;
15584 	string[] processedFilters;
15585 
15586 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"]
15587 	this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) {
15588 		super(owner, 500, 400, "Choose File..."); // owner);
15589 
15590 		foreach(filter; filters) {
15591 			while(filter.length && filter[0] != 0) {
15592 				filter = filter[1 .. $];
15593 			}
15594 			if(filter.length)
15595 				filter = filter[1 .. $]; // trim off the 0
15596 
15597 			while(filter.length) {
15598 				int idx = 0;
15599 				while(idx < filter.length && filter[idx] != ';') {
15600 					idx++;
15601 				}
15602 
15603 				processedFilters ~= filter[0 .. idx];
15604 				if(idx < filter.length)
15605 					idx++; // skip the ;
15606 				filter = filter[idx .. $];
15607 			}
15608 		}
15609 
15610 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
15611 
15612 		{
15613 			auto hl = new HorizontalLayout(this);
15614 			dirWidget = new ListWidget(hl);
15615 			listWidget = new ListWidget(hl);
15616 
15617 			// double click events normally trigger something else but
15618 			// here user might be clicking kinda fast and we'd rather just
15619 			// keep it
15620 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
15621 				auto ce = new ChangeEvent!void(dirWidget, () {});
15622 				ce.dispatch();
15623 			});
15624 
15625 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
15626 				string v;
15627 				foreach(o; dirWidget.options)
15628 					if(o.selected) {
15629 						v = o.label;
15630 						break;
15631 					}
15632 				if(v.length) {
15633 					currentDirectory ~= "/" ~ v;
15634 					loadFiles(currentDirectory, processedFilters);
15635 				}
15636 			});
15637 
15638 			// double click here, on the other hand, selects the file
15639 			// and moves on
15640 			listWidget.addEventListener((scope DoubleClickEvent dev) {
15641 				OK();
15642 			});
15643 		}
15644 
15645 		lineEdit = new LineEdit(this);
15646 		lineEdit.focus();
15647 		lineEdit.addEventListener(delegate(CharEvent event) {
15648 			if(event.character == '\t' || event.character == '\n')
15649 				event.preventDefault();
15650 		});
15651 
15652 		listWidget.addEventListener(EventType.change, () {
15653 			foreach(o; listWidget.options)
15654 				if(o.selected)
15655 					lineEdit.content = o.label;
15656 		});
15657 
15658 		loadFiles(currentDirectory, processedFilters);
15659 
15660 		lineEdit.addEventListener((KeyDownEvent event) {
15661 			if(event.key == Key.Tab) {
15662 
15663 				auto current = lineEdit.content;
15664 				if(current.length >= 2 && current[0 ..2] == "./")
15665 					current = current[2 .. $];
15666 
15667 				auto commonPrefix = loadFiles(currentDirectory, current ~ "*");
15668 
15669 				if(commonPrefix.length)
15670 					lineEdit.content = commonPrefix;
15671 
15672 				// FIXME: if that is a directory, add the slash? or even go inside?
15673 
15674 				event.preventDefault();
15675 			}
15676 		});
15677 
15678 		lineEdit.content = prefilledName;
15679 
15680 		auto hl = new HorizontalLayout(60, this);
15681 		auto cancelButton = new Button("Cancel", hl);
15682 		auto okButton = new Button("OK", hl);
15683 
15684 		cancelButton.addEventListener(EventType.triggered, &Cancel);
15685 		okButton.addEventListener(EventType.triggered, &OK);
15686 
15687 		this.addEventListener((KeyDownEvent event) {
15688 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
15689 				event.preventDefault();
15690 				OK();
15691 			}
15692 			if(event.key == Key.Escape)
15693 				Cancel();
15694 		});
15695 
15696 	}
15697 
15698 	override void OK() {
15699 		if(lineEdit.content.length) {
15700 			string accepted;
15701 			auto c = lineEdit.content;
15702 			if(c.length && c[0] == '/')
15703 				accepted = c;
15704 			else
15705 				accepted = currentDirectory ~ "/" ~ lineEdit.content;
15706 
15707 			if(isDir(accepted)) {
15708 				// FIXME: would be kinda nice to support ~ and collapse these paths too
15709 				// FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later.
15710 				currentDirectory = accepted;
15711 				loadFiles(currentDirectory, processedFilters);
15712 				lineEdit.content = "";
15713 				return;
15714 			}
15715 
15716 			if(onOK)
15717 				onOK(accepted);
15718 		}
15719 		close();
15720 	}
15721 
15722 	override void Cancel() {
15723 		if(onCancel)
15724 			onCancel();
15725 		close();
15726 	}
15727 }
15728 
15729 private bool isDir(string name) {
15730 	version(Windows) {
15731 		auto ws = WCharzBuffer(name);
15732 		auto ret = GetFileAttributesW(ws.ptr);
15733 		if(ret == INVALID_FILE_ATTRIBUTES)
15734 			return false;
15735 		return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0;
15736 	} else version(Posix) {
15737 		import core.sys.posix.sys.stat;
15738 		stat_t buf;
15739 		auto ret = stat((name ~ '\0').ptr, &buf);
15740 		if(ret == -1)
15741 			return false; // I could probably check more specific errors tbh
15742 		return (buf.st_mode & S_IFMT) == S_IFDIR;
15743 	} else return false;
15744 }
15745 
15746 /*
15747 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
15748 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
15749 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
15750 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
15751 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
15752 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
15753 http://www.sbin.org/doc/Xlib/chapt_03.html
15754 
15755 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
15756 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
15757 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
15758 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
15759 */
15760 
15761 
15762 // These are all for setMenuAndToolbarFromAnnotatedCode
15763 /// This item in the menu will be preceded by a separator line
15764 /// Group: generating_from_code
15765 struct separator {}
15766 deprecated("It was misspelled, use separator instead") alias seperator = separator;
15767 /// Program-wide keyboard shortcut to trigger the action
15768 /// Group: generating_from_code
15769 struct accelerator { string keyString; }
15770 /// tells which menu the action will be on
15771 /// Group: generating_from_code
15772 struct menu { string name; }
15773 /// Describes which toolbar section the action appears on
15774 /// Group: generating_from_code
15775 struct toolbar { string groupName; }
15776 ///
15777 /// Group: generating_from_code
15778 struct icon { ushort id; }
15779 ///
15780 /// Group: generating_from_code
15781 struct label { string label; }
15782 ///
15783 /// Group: generating_from_code
15784 struct hotkey { dchar ch; }
15785 ///
15786 /// Group: generating_from_code
15787 struct tip { string tip; }
15788 
15789 
15790 /++
15791 	Observes and allows inspection of an object via automatic gui
15792 +/
15793 /// Group: generating_from_code
15794 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
15795 	return new ObjectInspectionWindowImpl!(T)(t);
15796 }
15797 
15798 class ObjectInspectionWindow : Window {
15799 	this(int a, int b, string c) {
15800 		super(a, b, c);
15801 	}
15802 
15803 	abstract void readUpdatesFromObject();
15804 }
15805 
15806 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
15807 	T t;
15808 	this(T t) {
15809 		this.t = t;
15810 
15811 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
15812 
15813 		foreach(memberName; __traits(derivedMembers, T)) {{
15814 			alias member = I!(__traits(getMember, t, memberName))[0];
15815 			alias type = typeof(member);
15816 			static if(is(type == int)) {
15817 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
15818 				//le.addEventListener("char", (Event ev) {
15819 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
15820 						//ev.preventDefault();
15821 				//});
15822 				le.addEventListener(EventType.change, (Event ev) {
15823 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
15824 				});
15825 
15826 				updateMemberDelegates[memberName] = () {
15827 					le.content = toInternal!string(__traits(getMember, t, memberName));
15828 				};
15829 			}
15830 		}}
15831 	}
15832 
15833 	void delegate()[string] updateMemberDelegates;
15834 
15835 	override void readUpdatesFromObject() {
15836 		foreach(k, v; updateMemberDelegates)
15837 			v();
15838 	}
15839 }
15840 
15841 /++
15842 	Creates a dialog based on a data structure.
15843 
15844 	---
15845 	dialog(window, (YourStructure value) {
15846 		// the user filled in the struct and clicked OK,
15847 		// you can check the members now
15848 	});
15849 	---
15850 
15851 	Params:
15852 		initialData = the initial value to show in the dialog. It will not modify this unless
15853 		it is a class then it might, no promises.
15854 
15855 	History:
15856 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
15857 
15858 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
15859 		be deprecated soon.
15860 +/
15861 /// Group: generating_from_code
15862 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15863 	dialog(null, T.init, onOK, onCancel, title);
15864 }
15865 /// ditto
15866 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15867 	dialog(null, T.init, onOK, onCancel, title);
15868 }
15869 /// ditto
15870 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15871 	dialog(parent, T.init, onOK, onCancel, title);
15872 }
15873 /// ditto
15874 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15875 	dialog(parent, initialData, onOK, onCancel, title);
15876 }
15877 /// ditto
15878 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15879 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
15880 	dg.show();
15881 }
15882 
15883 private static template I(T...) { alias I = T; }
15884 
15885 
15886 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
15887 	if(name == "id")
15888 		return allLowerCase ? name : "ID";
15889 
15890 	char[160] buffer;
15891 	int bufferIndex = 0;
15892 	bool shouldCap = true;
15893 	bool shouldSpace;
15894 	bool lastWasCap;
15895 	foreach(idx, char ch; name) {
15896 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15897 
15898 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
15899 			if(lastWasCap) {
15900 				// two caps in a row, don't change. Prolly acronym.
15901 			} else {
15902 				if(idx)
15903 					shouldSpace = true; // new word, add space
15904 			}
15905 
15906 			lastWasCap = true;
15907 		} else {
15908 			lastWasCap = false;
15909 		}
15910 
15911 		if(shouldSpace) {
15912 			buffer[bufferIndex++] = space;
15913 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15914 			shouldSpace = false;
15915 		}
15916 		if(shouldCap) {
15917 			if(ch >= 'a' && ch <= 'z')
15918 				ch -= 32;
15919 			shouldCap = false;
15920 		}
15921 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
15922 			ch += 32;
15923 		buffer[bufferIndex++] = ch;
15924 	}
15925 	return buffer[0 .. bufferIndex].idup;
15926 }
15927 
15928 /++
15929 	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.
15930 +/
15931 class AutomaticDialog(T) : Dialog {
15932 	T t;
15933 
15934 	void delegate(T) onOK;
15935 	void delegate() onCancel;
15936 
15937 	override int paddingTop() { return defaultLineHeight; }
15938 	override int paddingBottom() { return defaultLineHeight; }
15939 	override int paddingRight() { return defaultLineHeight; }
15940 	override int paddingLeft() { return defaultLineHeight; }
15941 
15942 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
15943 		assert(onOK !is null);
15944 
15945 		t = initialData;
15946 
15947 		static if(is(T == class)) {
15948 			if(t is null)
15949 				t = new T();
15950 		}
15951 		this.onOK = onOK;
15952 		this.onCancel = onCancel;
15953 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
15954 
15955 		static if(is(T == class))
15956 			this.addDataControllerWidget(t);
15957 		else
15958 			this.addDataControllerWidget(&t);
15959 
15960 		auto hl = new HorizontalLayout(this);
15961 		auto stretch = new HorizontalSpacer(hl); // to right align
15962 		auto ok = new CommandButton("OK", hl);
15963 		auto cancel = new CommandButton("Cancel", hl);
15964 		ok.addEventListener(EventType.triggered, &OK);
15965 		cancel.addEventListener(EventType.triggered, &Cancel);
15966 
15967 		this.addEventListener((KeyDownEvent ev) {
15968 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
15969 				ok.focus();
15970 				OK();
15971 				ev.preventDefault();
15972 			}
15973 			if(ev.key == Key.Escape) {
15974 				Cancel();
15975 				ev.preventDefault();
15976 			}
15977 		});
15978 
15979 		this.addEventListener((scope ClosedEvent ce) {
15980 			if(onCancel)
15981 				onCancel();
15982 		});
15983 
15984 		//this.children[0].focus();
15985 	}
15986 
15987 	override void OK() {
15988 		onOK(t);
15989 		close();
15990 	}
15991 
15992 	override void Cancel() {
15993 		if(onCancel)
15994 			onCancel();
15995 		close();
15996 	}
15997 }
15998 
15999 private template baseClassCount(Class) {
16000 	private int helper() {
16001 		int count = 0;
16002 		static if(is(Class bases == super)) {
16003 			foreach(base; bases)
16004 				static if(is(base == class))
16005 					count += 1 + baseClassCount!base;
16006 		}
16007 		return count;
16008 	}
16009 
16010 	enum int baseClassCount = helper();
16011 }
16012 
16013 private long stringToLong(string s) {
16014 	long ret;
16015 	if(s.length == 0)
16016 		return ret;
16017 	bool negative = s[0] == '-';
16018 	if(negative)
16019 		s = s[1 .. $];
16020 	foreach(ch; s) {
16021 		if(ch >= '0' && ch <= '9') {
16022 			ret *= 10;
16023 			ret += ch - '0';
16024 		}
16025 	}
16026 	if(negative)
16027 		ret = -ret;
16028 	return ret;
16029 }
16030 
16031 
16032 interface ReflectableProperties {
16033 	/++
16034 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
16035 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
16036 		json in the current implementation.
16037 
16038 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
16039 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
16040 		as of the June 2, 2021 release.
16041 
16042 		History:
16043 			Added June 2, 2021.
16044 
16045 		See_Also: [getPropertyAsString], [setPropertyFromString]
16046 	+/
16047 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
16048 	/++
16049 		Requests a property to be delivered to you as a string, through your `sink` delegate.
16050 
16051 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
16052 		be interpreted as json, otherwise, it is just a plain string.
16053 
16054 		The sink should always be called exactly once for each call (it is basically a return value, but it might
16055 		use a local buffer it maintains instead of allocating a return value).
16056 
16057 		History:
16058 			Added June 2, 2021.
16059 
16060 		See_Also: [getPropertiesList], [setPropertyFromString]
16061 	+/
16062 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
16063 	/++
16064 		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.
16065 
16066 		History:
16067 			Added June 2, 2021.
16068 
16069 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
16070 	+/
16071 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
16072 
16073 	/// [setPropertyFromString] possible return values
16074 	enum SetPropertyResult {
16075 		success = 0, /// the property has been successfully set to the request value
16076 		notPermitted = -1, /// the property exists but it cannot be changed at this time
16077 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
16078 		noSuchProperty = -3, /// there is no property by that name
16079 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
16080 		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)
16081 	}
16082 
16083 	/++
16084 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
16085 
16086 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
16087 
16088 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
16089 		rarely need to use these building blocks directly.
16090 	+/
16091 	mixin template RegisterSetters() {
16092 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
16093 			switch(name) {
16094 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
16095 					case memberName:
16096 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
16097 							if(value != "true" && value != "false")
16098 								return SetPropertyResult.wrongFormat;
16099 							__traits(getMember, this, memberName) = value == "true" ? true : false;
16100 							return SetPropertyResult.success;
16101 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
16102 							import core.stdc.stdlib;
16103 							char[128] zero = 0;
16104 							if(buffer.length + 1 >= zero.length)
16105 								return SetPropertyResult.wrongFormat;
16106 							zero[0 .. buffer.length] = buffer[];
16107 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
16108 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
16109 							import core.stdc.stdlib;
16110 							char[128] zero = 0;
16111 							if(buffer.length + 1 >= zero.length)
16112 								return SetPropertyResult.wrongFormat;
16113 							zero[0 .. buffer.length] = buffer[];
16114 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
16115 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
16116 							__traits(getMember, this, memberName) = value.idup;
16117 						} else {
16118 							return SetPropertyResult.notImplemented;
16119 						}
16120 
16121 				}
16122 				default:
16123 					return super.setPropertyFromString(name, value, valueIsJson);
16124 			}
16125 		}
16126 	}
16127 
16128 	/++
16129 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
16130 
16131 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
16132 
16133 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
16134 		rarely need to use these building blocks directly.
16135 	+/
16136 	mixin template RegisterGetters() {
16137 		override void getPropertiesList(scope void delegate(string name) sink) const {
16138 			super.getPropertiesList(sink);
16139 
16140 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
16141 				sink(memberName);
16142 			}
16143 		}
16144 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
16145 			switch(name) {
16146 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
16147 					case memberName:
16148 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
16149 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
16150 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
16151 							import core.stdc.stdio;
16152 							char[32] buffer;
16153 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
16154 							sink(name, buffer[0 .. len], true);
16155 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
16156 							import core.stdc.stdio;
16157 							char[32] buffer;
16158 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
16159 							sink(name, buffer[0 .. len], true);
16160 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
16161 							sink(name, __traits(getMember, this, memberName), false);
16162 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
16163 						} else {
16164 							sink(name, null, true);
16165 						}
16166 
16167 					return;
16168 				}
16169 				default:
16170 					return super.getPropertyAsString(name, sink);
16171 			}
16172 		}
16173 	}
16174 }
16175 
16176 private struct Stack(T) {
16177 	this(int maxSize) {
16178 		internalLength = 0;
16179 		arr = initialBuffer[];
16180 	}
16181 
16182 	///.
16183 	void push(T t) {
16184 		if(internalLength >= arr.length) {
16185 			auto oldarr = arr;
16186 			if(arr.length < 4096)
16187 				arr = new T[arr.length * 2];
16188 			else
16189 				arr = new T[arr.length + 4096];
16190 			arr[0 .. oldarr.length] = oldarr[];
16191 		}
16192 
16193 		arr[internalLength] = t;
16194 		internalLength++;
16195 	}
16196 
16197 	///.
16198 	T pop() {
16199 		assert(internalLength);
16200 		internalLength--;
16201 		return arr[internalLength];
16202 	}
16203 
16204 	///.
16205 	T peek() {
16206 		assert(internalLength);
16207 		return arr[internalLength - 1];
16208 	}
16209 
16210 	///.
16211 	@property bool empty() {
16212 		return internalLength ? false : true;
16213 	}
16214 
16215 	///.
16216 	private T[] arr;
16217 	private size_t internalLength;
16218 	private T[64] initialBuffer;
16219 	// 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),
16220 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
16221 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
16222 }
16223 
16224 /// 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.
16225 private struct WidgetStream {
16226 
16227 	///.
16228 	@property Widget front() {
16229 		return current.widget;
16230 	}
16231 
16232 	/// Use Widget.tree instead.
16233 	this(Widget start) {
16234 		current.widget = start;
16235 		current.childPosition = -1;
16236 		isEmpty = false;
16237 		stack = typeof(stack)(0);
16238 	}
16239 
16240 	/*
16241 		Handle it
16242 		handle its children
16243 
16244 	*/
16245 
16246 	///.
16247 	void popFront() {
16248 	    more:
16249 	    	if(isEmpty) return;
16250 
16251 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
16252 
16253 		current.childPosition++;
16254 		if(current.childPosition >= current.widget.children.length) {
16255 			if(stack.empty())
16256 				isEmpty = true;
16257 			else {
16258 				current = stack.pop();
16259 				goto more;
16260 			}
16261 		} else {
16262 			stack.push(current);
16263 			current.widget = current.widget.children[current.childPosition];
16264 			current.childPosition = -1;
16265 		}
16266 	}
16267 
16268 	///.
16269 	@property bool empty() {
16270 		return isEmpty;
16271 	}
16272 
16273 	private:
16274 
16275 	struct Current {
16276 		Widget widget;
16277 		int childPosition;
16278 	}
16279 
16280 	Current current;
16281 
16282 	Stack!(Current) stack;
16283 
16284 	bool isEmpty;
16285 }
16286 
16287 
16288 /+
16289 
16290 	I could fix up the hierarchy kinda like this
16291 
16292 	class Widget {
16293 		Widget[] children() { return null; }
16294 	}
16295 	interface WidgetContainer {
16296 		Widget asWidget();
16297 		void addChild(Widget w);
16298 
16299 		// alias asWidget this; // but meh
16300 	}
16301 
16302 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
16303 
16304 	class Layout : Widget, WidgetContainer {}
16305 
16306 	class Window : WidgetContainer {}
16307 
16308 
16309 	All constructors that previously took Widgets should now take WidgetContainers instead
16310 
16311 
16312 
16313 	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".
16314 +/
16315 
16316 /+
16317 	LAYOUTS 2.0
16318 
16319 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
16320 
16321 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
16322 
16323 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
16324 
16325 	and even Paint can just use computedStyle...
16326 
16327 		background color
16328 		font
16329 		border color and style
16330 
16331 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
16332 		please note that many widgets and in some modes will completely ignore properties as they will.
16333 		they are just hints you set, not promises.
16334 
16335 
16336 
16337 
16338 
16339 	So generally the existing virtual functions are just the default for the class. But individual objects
16340 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
16341 +/
16342 
16343 /++
16344 	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.
16345 
16346 	History:
16347 		Added May 24, 2021.
16348 +/
16349 struct WidgetBackground {
16350 	/++
16351 		A background with the given solid color.
16352 	+/
16353 	this(Color color) {
16354 		this.color = color;
16355 	}
16356 
16357 	this(WidgetBackground bg) {
16358 		this = bg;
16359 	}
16360 
16361 	/++
16362 		Creates a widget from the string.
16363 
16364 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
16365 	+/
16366 	static WidgetBackground fromString(string s) {
16367 		return WidgetBackground(Color.fromString(s));
16368 	}
16369 
16370 	/++
16371 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
16372 
16373 		History:
16374 			Made `public` on December 18, 2022 (dub v10.10).
16375 	+/
16376 	Color color;
16377 }
16378 
16379 /++
16380 	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!)
16381 
16382 	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.
16383 
16384 	You should not inherit from this directly, but instead use [VisualTheme].
16385 
16386 	History:
16387 		Added May 8, 2021
16388 +/
16389 abstract class BaseVisualTheme {
16390 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
16391 	abstract void doPaint(Widget widget, WidgetPainter painter);
16392 
16393 	/+
16394 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
16395 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
16396 	+/
16397 
16398 	/++
16399 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
16400 		where the interpretation of the string varies for each property and may include things like measurement units.
16401 	+/
16402 	abstract string getPropertyString(Widget widget, string propertyName);
16403 
16404 	/++
16405 		Default background color of the window. Widgets also use this to simulate transparency.
16406 
16407 		Probably some shade of grey.
16408 	+/
16409 	abstract Color windowBackgroundColor();
16410 	abstract Color widgetBackgroundColor();
16411 	abstract Color foregroundColor();
16412 	abstract Color lightAccentColor();
16413 	abstract Color darkAccentColor();
16414 
16415 	/++
16416 		Colors used to indicate active selections in lists and text boxes, etc.
16417 	+/
16418 	abstract Color selectionForegroundColor();
16419 	/// ditto
16420 	abstract Color selectionBackgroundColor();
16421 
16422 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
16423 
16424 	/++
16425 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
16426 	+/
16427 	abstract OperatingSystemFont defaultFont(int dpi);
16428 
16429 	private OperatingSystemFont[int] defaultFontCache_;
16430 	private OperatingSystemFont defaultFontCached(int dpi) {
16431 		if(dpi !in defaultFontCache_) {
16432 			// FIXME: set this to false if X disconnect or if visual theme changes
16433 			defaultFontCache_[dpi] = defaultFont(dpi);
16434 		}
16435 		return defaultFontCache_[dpi];
16436 	}
16437 }
16438 
16439 /+
16440 	A widget should have:
16441 		classList
16442 		dataset
16443 		attributes
16444 		computedStyles
16445 		state (persistent)
16446 		dynamic state (focused, hover, etc)
16447 +/
16448 
16449 // visualTheme.computedStyle(this).paddingLeft
16450 
16451 
16452 /++
16453 	This is your entry point to create your own visual theme for custom widgets.
16454 
16455 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
16456 
16457 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
16458 +/
16459 abstract class VisualTheme(CRTP) : BaseVisualTheme {
16460 	override string getPropertyString(Widget widget, string propertyName) {
16461 		return null;
16462 	}
16463 
16464 	/+
16465 		mixin StyleOverride!Widget
16466 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
16467 		w.useStyleProperties(dg);
16468 	}
16469 	+/
16470 
16471 	final override void doPaint(Widget widget, WidgetPainter painter) {
16472 		auto derived = cast(CRTP) cast(void*) this;
16473 
16474 		scope void delegate(Widget, WidgetPainter) bestMatch;
16475 		int bestMatchScore;
16476 
16477 		static if(__traits(hasMember, CRTP, "paint"))
16478 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
16479 			static if(is(typeof(overload) Params == __parameters)) {
16480 				static assert(Params.length == 2);
16481 				static assert(is(Params[0] : Widget));
16482 				static assert(is(Params[1] == WidgetPainter));
16483 				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);
16484 
16485 				alias type = Params[0];
16486 				if(cast(type) widget) {
16487 					auto score = baseClassCount!type;
16488 
16489 					if(score > bestMatchScore) {
16490 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
16491 						bestMatchScore = score;
16492 					}
16493 				}
16494 			} else static assert(0, "paint should be a method.");
16495 		}
16496 
16497 		if(bestMatch)
16498 			bestMatch(widget, painter);
16499 		else
16500 			widget.paint(painter);
16501 	}
16502 
16503 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
16504 
16505 	// 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
16506 	// mixin Beautiful95Theme;
16507 	mixin DefaultLightTheme;
16508 
16509 	private static struct Cached {
16510 		// i prolly want to do this
16511 	}
16512 }
16513 
16514 /// ditto
16515 mixin template Beautiful95Theme() {
16516 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
16517 	override Color widgetBackgroundColor() { return Color.white; }
16518 	override Color foregroundColor() { return Color.black; }
16519 	override Color darkAccentColor() { return Color(172, 172, 172); }
16520 	override Color lightAccentColor() { return Color(223, 223, 223); }
16521 	override Color selectionForegroundColor() { return Color.white; }
16522 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
16523 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
16524 }
16525 
16526 /// ditto
16527 mixin template DefaultLightTheme() {
16528 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
16529 	override Color widgetBackgroundColor() { return Color.white; }
16530 	override Color foregroundColor() { return Color.black; }
16531 	override Color darkAccentColor() { return Color(172, 172, 172); }
16532 	override Color lightAccentColor() { return Color(223, 223, 223); }
16533 	override Color selectionForegroundColor() { return Color.white; }
16534 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
16535 	override OperatingSystemFont defaultFont(int dpi) {
16536 		version(Windows)
16537 			return new OperatingSystemFont("Segoe UI");
16538 		else static if(UsingSimpledisplayCocoa) {
16539 			return (new OperatingSystemFont()).loadDefault;
16540 		} else {
16541 			// FIXME: undo xft's scaling so we don't end up double scaled
16542 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
16543 		}
16544 	}
16545 }
16546 
16547 /// ditto
16548 mixin template DefaultDarkTheme() {
16549 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
16550 	override Color widgetBackgroundColor() { return Color.black; }
16551 	override Color foregroundColor() { return Color.white; }
16552 	override Color darkAccentColor() { return Color(20, 20, 20); }
16553 	override Color lightAccentColor() { return Color(80, 80, 80); }
16554 	override Color selectionForegroundColor() { return Color.white; }
16555 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
16556 	override OperatingSystemFont defaultFont(int dpi) {
16557 		version(Windows)
16558 			return new OperatingSystemFont("Segoe UI", 12);
16559 		else static if(UsingSimpledisplayCocoa) {
16560 			return (new OperatingSystemFont()).loadDefault;
16561 		} else {
16562 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
16563 		}
16564 	}
16565 }
16566 
16567 /// ditto
16568 alias DefaultTheme = DefaultLightTheme;
16569 
16570 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
16571 	/+
16572 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
16573 	Color windowBackgroundColor() { return Color(242, 242, 242); }
16574 	Color darkAccentColor() { return windowBackgroundColor; }
16575 	Color lightAccentColor() { return windowBackgroundColor; }
16576 	+/
16577 }
16578 
16579 /++
16580 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
16581 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
16582 
16583 	History:
16584 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16585 +/
16586 class StateChanged(alias field) : Event {
16587 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
16588 	override bool cancelable() const { return false; }
16589 	this(Widget target, typeof(field) newValue) {
16590 		this.newValue = newValue;
16591 		super(EventString, target);
16592 	}
16593 
16594 	typeof(field) newValue;
16595 }
16596 
16597 /++
16598 	Convenience function to add a `triggered` event listener.
16599 
16600 	Its implementation is simply `w.addEventListener("triggered", dg);`
16601 
16602 	History:
16603 		Added November 27, 2021 (dub v10.4)
16604 +/
16605 void addWhenTriggered(Widget w, void delegate() dg) {
16606 	w.addEventListener("triggered", dg);
16607 }
16608 
16609 /++
16610 	Observable varables can be added to widgets and when they are changed, it fires
16611 	off a [StateChanged] event so you can react to it.
16612 
16613 	It is implemented as a getter and setter property, along with another helper you
16614 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
16615 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
16616 	example.
16617 
16618 	History:
16619 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16620 +/
16621 mixin template Observable(T, string name) {
16622 	private T backing;
16623 
16624 	mixin(q{
16625 		void } ~ name ~ q{_changed (void delegate(T) dg) {
16626 			this.addEventListener((StateChanged!this_thing ev) {
16627 				dg(ev.newValue);
16628 			});
16629 		}
16630 
16631 		@property T } ~ name ~ q{ () {
16632 			return backing;
16633 		}
16634 
16635 		@property void } ~ name ~ q{ (T t) {
16636 			backing = t;
16637 			auto event = new StateChanged!this_thing(this, t);
16638 			event.dispatch();
16639 		}
16640 	});
16641 
16642 	mixin("private alias this_thing = " ~ name ~ ";");
16643 }
16644 
16645 
16646 private bool startsWith(string test, string thing) {
16647 	if(test.length < thing.length)
16648 		return false;
16649 	return test[0 .. thing.length] == thing;
16650 }
16651 
16652 private bool endsWith(string test, string thing) {
16653 	if(test.length < thing.length)
16654 		return false;
16655 	return test[$ - thing.length .. $] == thing;
16656 }
16657 
16658 // still do layout delegation
16659 // and... split off Window from Widget.
16660 
16661 version(minigui_screenshots)
16662 struct Screenshot {
16663 	string name;
16664 }
16665 
16666 version(minigui_screenshots)
16667 static if(__VERSION__ > 2092)
16668 mixin(q{
16669 shared static this() {
16670 	import core.runtime;
16671 
16672 	static UnitTestResult screenshotMagic() {
16673 		string name;
16674 
16675 		import arsd.png;
16676 
16677 		auto results = new Window();
16678 		auto button = new Button("do it", results);
16679 
16680 		Window.newWindowCreated = delegate(Window w) {
16681 			Timer timer;
16682 			timer = new Timer(250, {
16683 				auto img = w.win.takeScreenshot();
16684 				timer.destroy();
16685 
16686 				version(Windows)
16687 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
16688 				else
16689 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
16690 
16691 				w.close();
16692 			});
16693 		};
16694 
16695 		button.addWhenTriggered( {
16696 
16697 		foreach(test; __traits(getUnitTests, mixin(__MODULE__))) {
16698 			name = null;
16699 			static foreach(attr; __traits(getAttributes, test)) {
16700 				static if(is(typeof(attr) == Screenshot))
16701 					name = attr.name;
16702 			}
16703 			if(name.length) {
16704 				test();
16705 			}
16706 		}
16707 
16708 		});
16709 
16710 		results.loop();
16711 
16712 		return UnitTestResult(0, 0, false, false);
16713 	}
16714 
16715 
16716 	Runtime.extendedModuleUnitTester = &screenshotMagic;
16717 }
16718 });
16719 version(minigui_screenshots) {
16720 	version(unittest)
16721 		void main() {}
16722 	else static assert(0, "dont forget the -unittest flag to dmd");
16723 }
16724 
16725 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
16726 // FIXME: make multiple accelerators disambiguate based ona rgs
16727 // FIXME: MainWindow ctor should have same arg order as Window
16728 // FIXME: mainwindow ctor w/ client area size instead of total size.
16729 // 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.
16730 // FIXME: tri-state checkbox
16731 // FIXME: subordinate controls grouping...