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 		} else if(event.button == MouseButton.right) {
1317 			showContextMenu(event.clientX, event.clientY);
1318 		}
1319 	}
1320 	/// ditto
1321 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1322 	/// ditto
1323 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1324 	/// ditto
1325 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1326 	/// ditto
1327 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1328 	/// ditto
1329 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1330 	/// ditto
1331 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1332 	/// ditto
1333 	void defaultEventHandler_char(CharEvent event) {}
1334 	/// ditto
1335 	void defaultEventHandler_triggered(Event event) {}
1336 	/// ditto
1337 	void defaultEventHandler_change(Event event) {}
1338 	/// ditto
1339 	void defaultEventHandler_focus(Event event) {}
1340 	/// ditto
1341 	void defaultEventHandler_blur(Event event) {}
1342 	/// ditto
1343 	void defaultEventHandler_focusin(Event event) {}
1344 	/// ditto
1345 	void defaultEventHandler_focusout(Event event) {}
1346 
1347 	/++
1348 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1349 
1350 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1351 
1352 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1353 		of participating in handler delegation.
1354 
1355 		$(TIP
1356 			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.
1357 		)
1358 	+/
1359 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1360 		return addEventListener(event, (Widget, scope Event e) {
1361 			if(e.srcElement is this)
1362 				handler();
1363 		}, useCapture);
1364 	}
1365 
1366 	/// ditto
1367 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1368 		return addEventListener(event, (Widget, Event e) {
1369 			if(e.srcElement is this)
1370 				handler(e);
1371 		}, useCapture);
1372 	}
1373 
1374 	/// ditto
1375 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1376 		static if(is(Handler Fn == delegate)) {
1377 		static if(is(Fn Params == __parameters)) {
1378 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1379 				if(e.srcElement !is this)
1380 					return;
1381 				auto ty = cast(Params[0]) e;
1382 				if(ty !is null)
1383 					handler(ty);
1384 			}, useCapture);
1385 		} else static assert(0);
1386 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1387 	}
1388 
1389 	/// ditto
1390 	@scriptable
1391 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1392 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1393 	}
1394 
1395 	/// ditto
1396 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1397 		static if(is(Handler Fn == delegate)) {
1398 		static if(is(Fn Params == __parameters)) {
1399 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1400 				auto ty = cast(Params[0]) e;
1401 				if(ty !is null)
1402 					handler(ty);
1403 			}, useCapture);
1404 		} else static assert(0);
1405 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1406 	}
1407 
1408 	/// ditto
1409 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1410 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1411 	}
1412 
1413 	/// ditto
1414 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1415 		if(event.length > 2 && event[0..2] == "on")
1416 			event = event[2 .. $];
1417 
1418 		if(useCapture)
1419 			capturingEventHandlers[event] ~= handler;
1420 		else
1421 			bubblingEventHandlers[event] ~= handler;
1422 
1423 		return EventListener(this, event, handler, useCapture);
1424 	}
1425 
1426 	/// ditto
1427 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1428 		if(event.length > 2 && event[0..2] == "on")
1429 			event = event[2 .. $];
1430 
1431 		if(useCapture) {
1432 			if(event in capturingEventHandlers)
1433 			foreach(ref evt; capturingEventHandlers[event])
1434 				if(evt is handler) evt = null;
1435 		} else {
1436 			if(event in bubblingEventHandlers)
1437 			foreach(ref evt; bubblingEventHandlers[event])
1438 				if(evt is handler) evt = null;
1439 		}
1440 	}
1441 
1442 	/// ditto
1443 	void removeEventListener(EventListener listener) {
1444 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1445 	}
1446 
1447 	static if(UsingSimpledisplayX11) {
1448 		void discardXConnectionState() {
1449 			foreach(child; children)
1450 				child.discardXConnectionState();
1451 		}
1452 
1453 		void recreateXConnectionState() {
1454 			foreach(child; children)
1455 				child.recreateXConnectionState();
1456 			redraw();
1457 		}
1458 	}
1459 
1460 	/++
1461 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1462 
1463 		History:
1464 			`globalCoordinates` was made `final` on May 11, 2021.
1465 	+/
1466 	Point globalCoordinates() {
1467 		int x = this.x;
1468 		int y = this.y;
1469 		auto p = this.parent;
1470 		while(p) {
1471 			x += p.x;
1472 			y += p.y;
1473 			p = p.parent;
1474 		}
1475 
1476 		static if(UsingSimpledisplayX11) {
1477 			auto dpy = XDisplayConnection.get;
1478 			arsd.simpledisplay.Window dummyw;
1479 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1480 		} else version(Windows) {
1481 			POINT pt;
1482 			pt.x = x;
1483 			pt.y = y;
1484 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1485 			x = pt.x;
1486 			y = pt.y;
1487 		} else {
1488 			featureNotImplemented();
1489 		}
1490 
1491 		return Point(x, y);
1492 	}
1493 
1494 	version(win32_widgets)
1495 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1496 
1497 	version(win32_widgets)
1498 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1499 	void handleWmCommand(ushort cmd, ushort id) {}
1500 
1501 	version(win32_widgets)
1502 	/++
1503 		Called when a WM_NOTIFY is sent to the associated hwnd.
1504 
1505 		History:
1506 	+/
1507 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1508 
1509 	version(win32_widgets)
1510 	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); }
1511 
1512 	/++
1513 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1514 
1515 		Updates to this variable will only be made visible on the next mouse enter event.
1516 	+/
1517 	@scriptable string statusTip;
1518 	// string toolTip;
1519 	// string helpText;
1520 
1521 	/++
1522 		If true, this widget can be focused via keyboard control with the tab key.
1523 
1524 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1525 	+/
1526 	bool tabStop = true;
1527 	/++
1528 		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.)
1529 	+/
1530 	int tabOrder;
1531 
1532 	version(win32_widgets) {
1533 		static Widget[HWND] nativeMapping;
1534 		/// The native handle, if there is one.
1535 		HWND hwnd;
1536 		WNDPROC originalWindowProcedure;
1537 
1538 		SimpleWindow simpleWindowWrappingHwnd;
1539 
1540 		// please note it IGNORES your return value and does NOT forward it to Windows!
1541 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1542 			return 0;
1543 		}
1544 	}
1545 	private bool implicitlyCreated;
1546 
1547 	/// 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.
1548 	int x;
1549 	/// ditto
1550 	int y;
1551 	private int _width;
1552 	private int _height;
1553 	private Widget[] _children;
1554 	private Widget _parent;
1555 	private Window _parentWindow;
1556 
1557 	/++
1558 		Returns the window to which this widget is attached.
1559 
1560 		History:
1561 			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.
1562 	+/
1563 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1564 	private @property void parentWindow(Window parent) {
1565 		auto old = _parentWindow;
1566 		_parentWindow = parent;
1567 		newParentWindow(old, _parentWindow);
1568 		foreach(child; children)
1569 			child.parentWindow = parent; // please note that this is recursive
1570 	}
1571 
1572 	/++
1573 		Called when the widget has been added to or remove from a parent window.
1574 
1575 		Note that either oldParent and/or newParent may be null any time this is called.
1576 
1577 		History:
1578 			Added September 13, 2024
1579 	+/
1580 	protected void newParentWindow(Window oldParent, Window newParent) {}
1581 
1582 	/++
1583 		Returns the list of the widget's children.
1584 
1585 		History:
1586 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1587 
1588 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1589 	+/
1590 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1591 
1592 	/++
1593 		Returns the widget's parent.
1594 
1595 		History:
1596 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1597 
1598 			The parent should only be managed by the [addChild] and [removeWidget] method.
1599 	+/
1600 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1601 
1602 	/// The widget's current size.
1603 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1604 	/// ditto
1605 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1606 
1607 	/// Only the layout manager should be calling these.
1608 	final protected @property int width(int a) @safe { return _width = a; }
1609 	/// ditto
1610 	final protected @property int height(int a) @safe { return _height = a; }
1611 
1612 	/++
1613 		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.
1614 
1615 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
1616 	+/
1617 	protected void registerMovement() {
1618 		version(win32_widgets) {
1619 			if(hwnd) {
1620 				auto pos = getChildPositionRelativeToParentHwnd(this);
1621 				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
1622 				this.redraw();
1623 			}
1624 		}
1625 		sendResizeEvent();
1626 	}
1627 
1628 	/// Creates the widget and adds it to the parent.
1629 	this(Widget parent) {
1630 		if(parent !is null)
1631 			parent.addChild(this);
1632 		setupDefaultEventHandlers();
1633 	}
1634 
1635 	/// 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.
1636 	@scriptable
1637 	bool isFocused() {
1638 		return parentWindow && parentWindow.focusedWidget is this;
1639 	}
1640 
1641 	private bool showing_ = true;
1642 	///
1643 	bool showing() const { return showing_; }
1644 	///
1645 	bool hidden() const { return !showing_; }
1646 	/++
1647 		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.
1648 
1649 		Note that a widget only ever shows if all its parents are showing too.
1650 	+/
1651 	void showing(bool s, bool recalculate = true) {
1652 		if(s != showing_) {
1653 			showing_ = s;
1654 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
1655 
1656 			showNativeWindowChildren(s);
1657 
1658 			if(parent && recalculate) {
1659 				parent.queueRecomputeChildLayout();
1660 				parent.redraw();
1661 			}
1662 
1663 			if(s) {
1664 				queueRecomputeChildLayout();
1665 				redraw();
1666 			}
1667 		}
1668 	}
1669 	/// Convenience method for `showing = true`
1670 	@scriptable
1671 	void show() {
1672 		showing = true;
1673 	}
1674 	/// Convenience method for `showing = false`
1675 	@scriptable
1676 	void hide() {
1677 		showing = false;
1678 	}
1679 
1680 	/++
1681 		If you are a native window, show/hide it based on shouldShow and return `true`.
1682 
1683 		Otherwise, do nothing and return false.
1684 	+/
1685 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
1686 		version(win32_widgets) {
1687 			if(hwnd) {
1688 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
1689 				return true;
1690 			} else {
1691 				return false;
1692 			}
1693 		} else {
1694 			return false;
1695 		}
1696 	}
1697 
1698 	private void showNativeWindowChildren(bool s) {
1699 		if(!showOrHideIfNativeWindow(s && showing))
1700 			foreach(child; children)
1701 				child.showNativeWindowChildren(s);
1702 	}
1703 
1704 	///
1705 	@scriptable
1706 	void focus() {
1707 		assert(parentWindow !is null);
1708 		if(isFocused())
1709 			return;
1710 
1711 		if(parentWindow.focusedWidget) {
1712 			// FIXME: more details here? like from and to
1713 			auto from = parentWindow.focusedWidget;
1714 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
1715 			parentWindow.focusedWidget = null;
1716 			from.emit!BlurEvent();
1717 			this.emit!FocusOutEvent();
1718 		}
1719 
1720 
1721 		version(win32_widgets) {
1722 			if(this.hwnd !is null)
1723 				SetFocus(this.hwnd);
1724 		}
1725 		//else static if(UsingSimpledisplayX11)
1726 			//this.parentWindow.win.focus();
1727 
1728 		parentWindow.focusedWidget = this;
1729 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
1730 		this.emit!FocusEvent();
1731 		this.emit!FocusInEvent();
1732 	}
1733 
1734 	/+
1735 	/++
1736 		Unfocuses the widget. This may reset
1737 	+/
1738 	@scriptable
1739 	void blur() {
1740 
1741 	}
1742 	+/
1743 
1744 
1745 	/++
1746 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
1747 
1748 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
1749 	+/
1750 	void attachedToWindow(Window w) {}
1751 	/++
1752 		Callback when the widget is added to another widget.
1753 
1754 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
1755 	+/
1756 	void addedTo(Widget w) {}
1757 
1758 	/++
1759 		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.
1760 
1761 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
1762 	+/
1763 	protected void addChild(Widget w, int position = int.max) {
1764 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
1765 		assert(w !is this, "Child cannot be its own parent!");
1766 		w._parent = this;
1767 		if(position == int.max || position == children.length) {
1768 			_children ~= w;
1769 		} else {
1770 			assert(position < _children.length);
1771 			_children.length = _children.length + 1;
1772 			for(int i = cast(int) _children.length - 1; i > position; i--)
1773 				_children[i] = _children[i - 1];
1774 			_children[position] = w;
1775 		}
1776 
1777 		this.parentWindow = this._parentWindow;
1778 
1779 		w.addedTo(this);
1780 
1781 		bool parentIsNative;
1782 		version(win32_widgets) {
1783 			parentIsNative = hwnd !is null;
1784 		}
1785 		if(!parentIsNative && !showing)
1786 			w.showOrHideIfNativeWindow(false);
1787 
1788 		if(parentWindow !is null) {
1789 			w.attachedToWindow(parentWindow);
1790 			parentWindow.queueRecomputeChildLayout();
1791 			parentWindow.redraw();
1792 		}
1793 	}
1794 
1795 	/++
1796 		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.
1797 	+/
1798 	Widget getChildAtPosition(int x, int y) {
1799 		// it goes backward so the last one to show gets picked first
1800 		// might use z-index later
1801 		foreach_reverse(child; children) {
1802 			if(child.hidden)
1803 				continue;
1804 			if(child.x <= x && child.y <= y
1805 				&& ((x - child.x) < child.width)
1806 				&& ((y - child.y) < child.height))
1807 			{
1808 				return child;
1809 			}
1810 		}
1811 
1812 		return null;
1813 	}
1814 
1815 	/++
1816 		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.
1817 
1818 		History:
1819 			Added July 2, 2021 (v10.2)
1820 	+/
1821 	protected void addScrollPosition(ref int x, ref int y) {};
1822 
1823 	/++
1824 		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.
1825 
1826 		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.
1827 
1828 		[paint] is not called for system widgets as the OS library draws them instead.
1829 
1830 
1831 		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.
1832 
1833 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
1834 
1835 		History:
1836 			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.
1837 	+/
1838 	void paint(WidgetPainter painter) {
1839 		version(win32_widgets)
1840 			if(hwnd) {
1841 				return;
1842 			}
1843 		painter.drawThemed(&paintContent); // note this refers to the following overload
1844 	}
1845 
1846 	/++
1847 		Responsible for drawing the content as the theme engine is responsible for other elements.
1848 
1849 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
1850 
1851 		Params:
1852 			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.
1853 
1854 			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.
1855 
1856 			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.
1857 
1858 		Returns:
1859 			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.
1860 
1861 		History:
1862 			Added May 15, 2021
1863 	+/
1864 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
1865 		return bounds;
1866 	}
1867 
1868 	deprecated("Change ScreenPainter to WidgetPainter")
1869 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
1870 
1871 	/// I don't actually like the name of this
1872 	/// this draws a background on it
1873 	void erase(WidgetPainter painter) {
1874 		version(win32_widgets)
1875 			if(hwnd) return; // Windows will do it. I think.
1876 
1877 		auto c = getComputedStyle().background.color;
1878 		painter.fillColor = c;
1879 		painter.outlineColor = c;
1880 
1881 		version(win32_widgets) {
1882 			HANDLE b, p;
1883 			if(c.a == 0 && parent is parentWindow) {
1884 				// I don't remember why I had this really...
1885 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
1886 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
1887 			}
1888 		}
1889 		painter.drawRectangle(Point(0, 0), width, height);
1890 		version(win32_widgets) {
1891 			if(c.a == 0 && parent is parentWindow) {
1892 				SelectObject(painter.impl.hdc, p);
1893 				SelectObject(painter.impl.hdc, b);
1894 			}
1895 		}
1896 	}
1897 
1898 	///
1899 	WidgetPainter draw() {
1900 		int x = this.x, y = this.y;
1901 		auto parent = this.parent;
1902 		while(parent) {
1903 			x += parent.x;
1904 			y += parent.y;
1905 			parent = parent.parent;
1906 		}
1907 
1908 		auto painter = parentWindow.win.draw(true);
1909 		painter.originX = x;
1910 		painter.originY = y;
1911 		painter.setClipRectangle(Point(0, 0), width, height);
1912 		return WidgetPainter(painter, this);
1913 	}
1914 
1915 	/// 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.
1916 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
1917 		if(hidden)
1918 			return;
1919 
1920 		int paintX = x;
1921 		int paintY = y;
1922 		if(this.useNativeDrawing()) {
1923 			paintX = 0;
1924 			paintY = 0;
1925 			lox = 0;
1926 			loy = 0;
1927 			containment = Rectangle(0, 0, int.max, int.max);
1928 		}
1929 
1930 		painter.originX = lox + paintX;
1931 		painter.originY = loy + paintY;
1932 
1933 		bool actuallyPainted = false;
1934 
1935 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
1936 		if(clip == Rectangle.init) {
1937 			// writeln(this, " clipped out");
1938 			return;
1939 		}
1940 
1941 		bool invalidateChildren = invalidate;
1942 
1943 		if(redrawRequested || force) {
1944 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
1945 
1946 			painter.drawingUpon = this;
1947 
1948 			erase(painter);
1949 			if(painter.visualTheme)
1950 				painter.visualTheme.doPaint(this, painter);
1951 			else
1952 				paint(painter);
1953 
1954 			if(invalidate) {
1955 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
1956 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
1957 				painter.invalidateRect(region);
1958 				// children are contained inside this, so no need to do extra work
1959 				invalidateChildren = false;
1960 			}
1961 
1962 			redrawRequested = false;
1963 			actuallyPainted = true;
1964 		}
1965 
1966 		foreach(child; children) {
1967 			version(win32_widgets)
1968 				if(child.useNativeDrawing()) continue;
1969 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
1970 		}
1971 
1972 		version(win32_widgets)
1973 		foreach(child; children) {
1974 			if(child.useNativeDrawing) {
1975 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
1976 				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
1977 			}
1978 		}
1979 	}
1980 
1981 	protected bool useNativeDrawing() nothrow {
1982 		version(win32_widgets)
1983 			return hwnd !is null;
1984 		else
1985 			return false;
1986 	}
1987 
1988 	private static class RedrawEvent {}
1989 	private __gshared re = new RedrawEvent();
1990 
1991 	private bool redrawRequested;
1992 	///
1993 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
1994 		redrawRequested = true;
1995 
1996 		if(this.parentWindow) {
1997 			auto sw = this.parentWindow.win;
1998 			assert(sw !is null);
1999 			if(!sw.eventQueued!RedrawEvent) {
2000 				sw.postEvent(re);
2001 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2002 			}
2003 		}
2004 	}
2005 
2006 	private SimpleWindow drawableWindow;
2007 
2008 	/++
2009 		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.
2010 
2011 		Returns:
2012 			`true` if you should do your default behavior.
2013 
2014 		History:
2015 			Added May 5, 2021
2016 
2017 		Bugs:
2018 			It does not do the static checks on gdc right now.
2019 	+/
2020 	final protected bool emit(EventType, this This, Args...)(Args args) {
2021 		version(GNU) {} else
2022 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2023 		auto e = new EventType(this, args);
2024 		e.dispatch();
2025 		return !e.defaultPrevented;
2026 	}
2027 	/// ditto
2028 	final protected bool emit(string eventString, this This)() {
2029 		auto e = new Event(eventString, this);
2030 		e.dispatch();
2031 		return !e.defaultPrevented;
2032 	}
2033 
2034 	/++
2035 		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.
2036 
2037 		History:
2038 			Added May 5, 2021
2039 	+/
2040 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2041 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2042 		return addEventListener(handler);
2043 	}
2044 
2045 	/++
2046 		Gets the computed style properties from the visual theme.
2047 
2048 		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].)
2049 
2050 		History:
2051 			Added May 8, 2021
2052 	+/
2053 	final StyleInformation getComputedStyle() {
2054 		return StyleInformation(this);
2055 	}
2056 
2057 	int focusableWidgets(scope int delegate(Widget) dg) {
2058 		foreach(widget; WidgetStream(this)) {
2059 			if(widget.tabStop && !widget.hidden) {
2060 				int result = dg(widget);
2061 				if (result)
2062 					return result;
2063 			}
2064 		}
2065 		return 0;
2066 	}
2067 
2068 	/++
2069 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2070 		for the given content box (the area between the padding)
2071 
2072 		History:
2073 			Added January 4, 2023 (dub v11.0)
2074 	+/
2075 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2076 		auto cs = getComputedStyle();
2077 
2078 		auto borderWidth = getBorderWidth(cs.borderStyle);
2079 
2080 		auto rect = contentBox;
2081 
2082 		rect.left -= borderWidth;
2083 		rect.right += borderWidth;
2084 		rect.top -= borderWidth;
2085 		rect.bottom += borderWidth;
2086 
2087 		auto insideBorderRect = rect;
2088 
2089 		rect.left -= cs.paddingLeft;
2090 		rect.right += cs.paddingRight;
2091 		rect.top -= cs.paddingTop;
2092 		rect.bottom += cs.paddingBottom;
2093 
2094 		return rect;
2095 	}
2096 
2097 
2098 	// FIXME: I kinda want to hide events from implementation widgets
2099 	// so it just catches them all and stops propagation...
2100 	// i guess i can do it with a event listener on star.
2101 
2102 	mixin Emits!KeyDownEvent; ///
2103 	mixin Emits!KeyUpEvent; ///
2104 	mixin Emits!CharEvent; ///
2105 
2106 	mixin Emits!MouseDownEvent; ///
2107 	mixin Emits!MouseUpEvent; ///
2108 	mixin Emits!ClickEvent; ///
2109 	mixin Emits!DoubleClickEvent; ///
2110 	mixin Emits!MouseMoveEvent; ///
2111 	mixin Emits!MouseOverEvent; ///
2112 	mixin Emits!MouseOutEvent; ///
2113 	mixin Emits!MouseEnterEvent; ///
2114 	mixin Emits!MouseLeaveEvent; ///
2115 
2116 	mixin Emits!ResizeEvent; ///
2117 
2118 	mixin Emits!BlurEvent; ///
2119 	mixin Emits!FocusEvent; ///
2120 
2121 	mixin Emits!FocusInEvent; ///
2122 	mixin Emits!FocusOutEvent; ///
2123 }
2124 
2125 /+
2126 /++
2127 	Interface to indicate that the widget has a simple value property.
2128 
2129 	History:
2130 		Added August 26, 2021
2131 +/
2132 interface HasValue!T {
2133 	/// Getter
2134 	@property T value();
2135 	/// Setter
2136 	@property void value(T);
2137 }
2138 
2139 /++
2140 	Interface to indicate that the widget has a range of possible values for its simple value property.
2141 	This would be present on something like a slider or possibly a number picker.
2142 
2143 	History:
2144 		Added September 11, 2021
2145 +/
2146 interface HasRangeOfValues!T : HasValue!T {
2147 	/// The minimum and maximum values in the range, inclusive.
2148 	@property T minValue();
2149 	@property void minValue(T); /// ditto
2150 	@property T maxValue(); /// ditto
2151 	@property void maxValue(T); /// ditto
2152 
2153 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2154 	@property void step(T);
2155 	@property T step(); /// ditto
2156 }
2157 
2158 /++
2159 	Interface to indicate that the widget has a list of possible values the user can choose from.
2160 	This would be present on something like a drop-down selector.
2161 
2162 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2163 	combobox.
2164 
2165 	History:
2166 		Added September 11, 2021
2167 +/
2168 interface HasListOfValues!T : HasValue!T {
2169 	@property T[] values;
2170 	@property void values(T[]);
2171 
2172 	@property int selectedIndex(); // note it may return -1!
2173 	@property void selectedIndex(int);
2174 }
2175 +/
2176 
2177 /++
2178 	History:
2179 		Added September 2021 (dub v10.4)
2180 +/
2181 class GridLayout : Layout {
2182 
2183 	// 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.
2184 
2185 	/++
2186 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2187 	+/
2188 	enum Gravity {
2189 		Center    = 0,
2190 		NorthWest = North | West,
2191 		North     = 0b10_00,
2192 		NorthEast = North | East,
2193 		West      = 0b00_10,
2194 		East      = 0b00_01,
2195 		SouthWest = South | West,
2196 		South     = 0b01_00,
2197 		SouthEast = South | East,
2198 	}
2199 
2200 	/++
2201 		The width and height are in some proportional units and can often just be 12.
2202 	+/
2203 	this(int width, int height, Widget parent) {
2204 		this.gridWidth = width;
2205 		this.gridHeight = height;
2206 		super(parent);
2207 	}
2208 
2209 	/++
2210 		Sets the position of the given child.
2211 
2212 		The units of these arguments are in the proportional grid units you set in the constructor.
2213 	+/
2214 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2215 		// ensure it is in bounds
2216 		// then ensure no overlaps
2217 
2218 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2219 
2220 		foreach(ref position; positions) {
2221 			if(position.widget is child) {
2222 				position = p;
2223 				goto set;
2224 			}
2225 		}
2226 
2227 		positions ~= p;
2228 
2229 		set:
2230 
2231 		// FIXME: should this batch?
2232 		queueRecomputeChildLayout();
2233 
2234 		return child;
2235 	}
2236 
2237 	override void addChild(Widget w, int position = int.max) {
2238 		super.addChild(w, position);
2239 		//positions ~= ChildPosition(w);
2240 		if(position != int.max) {
2241 			// FIXME: align it so they actually match.
2242 		}
2243 	}
2244 
2245 	override void widgetRemoved(size_t idx, Widget w) {
2246 		// FIXME: keep the positions array aligned
2247 		// positions[idx].widget = null;
2248 	}
2249 
2250 	override void recomputeChildLayout() {
2251 		registerMovement();
2252 		int onGrid = cast(int) positions.length;
2253 		c: foreach(child; children) {
2254 			// just snap it to the grid
2255 			if(onGrid)
2256 			foreach(position; positions)
2257 				if(position.widget is child) {
2258 					child.x = this.width * position.x / this.gridWidth;
2259 					child.y = this.height * position.y / this.gridHeight;
2260 					child.width = this.width * position.width / this.gridWidth;
2261 					child.height = this.height * position.height / this.gridHeight;
2262 
2263 					auto diff = child.width - child.maxWidth();
2264 					// FIXME: gravity?
2265 					if(diff > 0) {
2266 						child.width = child.width - diff;
2267 
2268 						if(position.gravity & Gravity.West) {
2269 							// nothing needed, already aligned
2270 						} else if(position.gravity & Gravity.East) {
2271 							child.x += diff;
2272 						} else {
2273 							child.x += diff / 2;
2274 						}
2275 					}
2276 
2277 					diff = child.height - child.maxHeight();
2278 					// FIXME: gravity?
2279 					if(diff > 0) {
2280 						child.height = child.height - diff;
2281 
2282 						if(position.gravity & Gravity.North) {
2283 							// nothing needed, already aligned
2284 						} else if(position.gravity & Gravity.South) {
2285 							child.y += diff;
2286 						} else {
2287 							child.y += diff / 2;
2288 						}
2289 					}
2290 
2291 
2292 					child.recomputeChildLayout();
2293 					onGrid--;
2294 					continue c;
2295 				}
2296 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2297 		}
2298 	}
2299 
2300 	private struct ChildPosition {
2301 		Widget widget;
2302 		int x;
2303 		int y;
2304 		int width;
2305 		int height;
2306 		Gravity gravity;
2307 	}
2308 	private ChildPosition[] positions;
2309 
2310 	int gridWidth = 12;
2311 	int gridHeight = 12;
2312 }
2313 
2314 ///
2315 abstract class ComboboxBase : Widget {
2316 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2317 	// or to always show the list, we want CBS_SIMPLE == 1
2318 	version(win32_widgets)
2319 		this(uint style, Widget parent) {
2320 			super(parent);
2321 			createWin32Window(this, "ComboBox"w, null, style);
2322 		}
2323 	else version(custom_widgets)
2324 		this(Widget parent) {
2325 			super(parent);
2326 
2327 			addEventListener((KeyDownEvent event) {
2328 				if(event.key == Key.Up) {
2329 					if(selection_ > -1) { // -1 means select blank
2330 						selection_--;
2331 						fireChangeEvent();
2332 					}
2333 					event.preventDefault();
2334 				}
2335 				if(event.key == Key.Down) {
2336 					if(selection_ + 1 < options.length) {
2337 						selection_++;
2338 						fireChangeEvent();
2339 					}
2340 					event.preventDefault();
2341 				}
2342 
2343 			});
2344 
2345 		}
2346 	else static assert(false);
2347 
2348 	/++
2349 		Returns the current list of options in the selection.
2350 
2351 		History:
2352 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2353 	+/
2354 	final @property string[] options() const {
2355 		return cast(string[]) options_;
2356 	}
2357 
2358 	private string[] options_;
2359 	private int selection_ = -1;
2360 
2361 	/++
2362 		Adds an option to the end of options array.
2363 	+/
2364 	void addOption(string s) {
2365 		options_ ~= s;
2366 		version(win32_widgets)
2367 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2368 	}
2369 
2370 	/++
2371 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2372 	+/
2373 	int getSelection() {
2374 		return selection_;
2375 	}
2376 
2377 	/++
2378 		Returns the current selection as a string.
2379 
2380 		History:
2381 			Added November 17, 2021
2382 	+/
2383 	string getSelectionString() {
2384 		return selection_ == -1 ? null : options[selection_];
2385 	}
2386 
2387 	/++
2388 		Sets the current selection to an index in the options array, or to the given option if present.
2389 		Please note that the string version may do a linear lookup.
2390 
2391 		Returns:
2392 			the index you passed in
2393 
2394 		History:
2395 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2396 
2397 			The return value was `void` prior to March 1, 2022.
2398 	+/
2399 	int setSelection(int idx) {
2400 		selection_ = idx;
2401 		version(win32_widgets)
2402 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2403 
2404 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2405 		t.dispatch();
2406 
2407 		return idx;
2408 	}
2409 
2410 	/// ditto
2411 	int setSelection(string s) {
2412 		if(s !is null)
2413 		foreach(idx, item; options)
2414 			if(item == s) {
2415 				return setSelection(cast(int) idx);
2416 			}
2417 		return setSelection(-1);
2418 	}
2419 
2420 	/++
2421 		This event is fired when the selection changes. Note it inherits
2422 		from ChangeEvent!string, meaning you can use that as well, and it also
2423 		fills in [Event.intValue].
2424 	+/
2425 	static class SelectionChangedEvent : ChangeEvent!string {
2426 		this(Widget target, int iv, string sv) {
2427 			super(target, &stringValue);
2428 			this.iv = iv;
2429 			this.sv = sv;
2430 		}
2431 		immutable int iv;
2432 		immutable string sv;
2433 
2434 		override @property string stringValue() { return sv; }
2435 		override @property int intValue() { return iv; }
2436 	}
2437 
2438 	version(win32_widgets)
2439 	override void handleWmCommand(ushort cmd, ushort id) {
2440 		if(cmd == CBN_SELCHANGE) {
2441 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2442 			fireChangeEvent();
2443 		}
2444 	}
2445 
2446 	private void fireChangeEvent() {
2447 		if(selection_ >= options.length)
2448 			selection_ = -1;
2449 
2450 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2451 		t.dispatch();
2452 	}
2453 
2454 	version(win32_widgets) {
2455 		override int minHeight() { return defaultLineHeight + 6; }
2456 		override int maxHeight() { return defaultLineHeight + 6; }
2457 	} else {
2458 		override int minHeight() { return defaultLineHeight + 4; }
2459 		override int maxHeight() { return defaultLineHeight + 4; }
2460 	}
2461 
2462 	version(custom_widgets) {
2463 
2464 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2465 
2466 		SimpleWindow dropDown;
2467 		void popup() {
2468 			auto w = width;
2469 			// FIXME: suggestedDropdownHeight see below
2470 			auto h = cast(int) this.options.length * defaultLineHeight + 8;
2471 
2472 			auto coord = this.globalCoordinates();
2473 			auto dropDown = new SimpleWindow(
2474 				w, h,
2475 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null);
2476 
2477 			dropDown.move(coord.x, coord.y + this.height);
2478 
2479 			{
2480 				auto cs = getComputedStyle();
2481 				auto painter = dropDown.draw();
2482 				draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2483 				auto p = Point(4, 4);
2484 				painter.outlineColor = cs.foregroundColor;
2485 				foreach(option; options) {
2486 					painter.drawText(p, option);
2487 					p.y += defaultLineHeight;
2488 				}
2489 			}
2490 
2491 			dropDown.setEventHandlers(
2492 				(MouseEvent event) {
2493 					if(event.type == MouseEventType.buttonReleased) {
2494 						dropDown.close();
2495 						auto element = (event.y - 4) / defaultLineHeight;
2496 						if(element >= 0 && element <= options.length) {
2497 							selection_ = element;
2498 
2499 							fireChangeEvent();
2500 						}
2501 					}
2502 				}
2503 			);
2504 
2505 			dropDown.visibilityChanged = (bool visible) {
2506 				if(visible) {
2507 					this.redraw();
2508 					dropDown.grabInput();
2509 				} else {
2510 					dropDown.releaseInputGrab();
2511 				}
2512 			};
2513 
2514 			dropDown.show();
2515 		}
2516 
2517 	}
2518 }
2519 
2520 /++
2521 	A drop-down list where the user must select one of the
2522 	given options. Like `<select>` in HTML.
2523 +/
2524 class DropDownSelection : ComboboxBase {
2525 	this(Widget parent) {
2526 		version(win32_widgets)
2527 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
2528 		else version(custom_widgets) {
2529 			super(parent);
2530 
2531 			addEventListener("focus", () { this.redraw; });
2532 			addEventListener("blur", () { this.redraw; });
2533 			addEventListener(EventType.change, () { this.redraw; });
2534 			addEventListener("mousedown", () { this.focus(); this.popup(); });
2535 			addEventListener((KeyDownEvent event) {
2536 				if(event.key == Key.Space)
2537 					popup();
2538 			});
2539 		} else static assert(false);
2540 	}
2541 
2542 	mixin Padding!q{2};
2543 	static class Style : Widget.Style {
2544 		override FrameStyle borderStyle() { return FrameStyle.risen; }
2545 	}
2546 	mixin OverrideStyle!Style;
2547 
2548 	version(custom_widgets)
2549 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2550 		auto cs = getComputedStyle();
2551 
2552 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
2553 
2554 		painter.outlineColor = cs.foregroundColor;
2555 		painter.fillColor = cs.foregroundColor;
2556 
2557 		/+
2558 		Point[4] triangle;
2559 		enum padding = 6;
2560 		enum paddingV = 7;
2561 		enum triangleWidth = 10;
2562 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
2563 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
2564 		triangle[2] = Point(width - padding - 0, paddingV);
2565 		triangle[3] = triangle[0];
2566 		painter.drawPolygon(triangle[]);
2567 		+/
2568 
2569 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
2570 
2571 		painter.drawPolygon(
2572 			scaleWithDpi(Point(2, 6) + offset),
2573 			scaleWithDpi(Point(7, 11) + offset),
2574 			scaleWithDpi(Point(12, 6) + offset),
2575 			scaleWithDpi(Point(2, 6) + offset)
2576 		);
2577 
2578 
2579 		return bounds;
2580 	}
2581 
2582 	version(win32_widgets)
2583 	override void registerMovement() {
2584 		version(win32_widgets) {
2585 			if(hwnd) {
2586 				auto pos = getChildPositionRelativeToParentHwnd(this);
2587 				// the height given to this from Windows' perspective is supposed
2588 				// to include the drop down's height. so I add to it to give some
2589 				// room for that.
2590 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
2591 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
2592 			}
2593 		}
2594 		sendResizeEvent();
2595 	}
2596 }
2597 
2598 /++
2599 	A text box with a drop down arrow listing selections.
2600 	The user can choose from the list, or type their own.
2601 +/
2602 class FreeEntrySelection : ComboboxBase {
2603 	this(Widget parent) {
2604 		version(win32_widgets)
2605 			super(2 /* CBS_DROPDOWN */, parent);
2606 		else version(custom_widgets) {
2607 			super(parent);
2608 			auto hl = new HorizontalLayout(this);
2609 			lineEdit = new LineEdit(hl);
2610 
2611 			tabStop = false;
2612 
2613 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2614 
2615 			auto btn = new class ArrowButton {
2616 				this() {
2617 					super(ArrowDirection.down, hl);
2618 				}
2619 				override int maxHeight() {
2620 					return lineEdit.maxHeight;
2621 				}
2622 			};
2623 			//btn.addDirectEventListener("focus", &lineEdit.focus);
2624 			btn.addEventListener("triggered", &this.popup);
2625 			addEventListener(EventType.change, (Event event) {
2626 				lineEdit.content = event.stringValue;
2627 				lineEdit.focus();
2628 				redraw();
2629 			});
2630 		}
2631 		else static assert(false);
2632 	}
2633 
2634 	version(custom_widgets) {
2635 		LineEdit lineEdit;
2636 	}
2637 }
2638 
2639 /++
2640 	A combination of free entry with a list below it.
2641 +/
2642 class ComboBox : ComboboxBase {
2643 	this(Widget parent) {
2644 		version(win32_widgets)
2645 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
2646 		else version(custom_widgets) {
2647 			super(parent);
2648 			lineEdit = new LineEdit(this);
2649 			listWidget = new ListWidget(this);
2650 			listWidget.multiSelect = false;
2651 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
2652 				string c = null;
2653 				foreach(option; listWidget.options)
2654 					if(option.selected) {
2655 						c = option.label;
2656 						break;
2657 					}
2658 				lineEdit.content = c;
2659 			});
2660 
2661 			listWidget.tabStop = false;
2662 			this.tabStop = false;
2663 			listWidget.addEventListener("focus", &lineEdit.focus);
2664 			this.addEventListener("focus", &lineEdit.focus);
2665 
2666 			addDirectEventListener(EventType.change, {
2667 				listWidget.setSelection(selection_);
2668 				if(selection_ != -1)
2669 					lineEdit.content = options[selection_];
2670 				lineEdit.focus();
2671 				redraw();
2672 			});
2673 
2674 			lineEdit.addEventListener("focus", &lineEdit.selectAll);
2675 
2676 			listWidget.addDirectEventListener(EventType.change, {
2677 				int set = -1;
2678 				foreach(idx, opt; listWidget.options)
2679 					if(opt.selected) {
2680 						set = cast(int) idx;
2681 						break;
2682 					}
2683 				if(set != selection_)
2684 					this.setSelection(set);
2685 			});
2686 		} else static assert(false);
2687 	}
2688 
2689 	override int minHeight() { return defaultLineHeight * 3; }
2690 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
2691 	override int heightStretchiness() { return 5; }
2692 
2693 	version(custom_widgets) {
2694 		LineEdit lineEdit;
2695 		ListWidget listWidget;
2696 
2697 		override void addOption(string s) {
2698 			listWidget.options ~= ListWidget.Option(s);
2699 			ComboboxBase.addOption(s);
2700 		}
2701 	}
2702 }
2703 
2704 /+
2705 class Spinner : Widget {
2706 	version(win32_widgets)
2707 	this(Widget parent) {
2708 		super(parent);
2709 		parentWindow = parent.parentWindow;
2710 		auto hlayout = new HorizontalLayout(this);
2711 		lineEdit = new LineEdit(hlayout);
2712 		upDownControl = new UpDownControl(hlayout);
2713 	}
2714 
2715 	LineEdit lineEdit;
2716 	UpDownControl upDownControl;
2717 }
2718 
2719 class UpDownControl : Widget {
2720 	version(win32_widgets)
2721 	this(Widget parent) {
2722 		super(parent);
2723 		parentWindow = parent.parentWindow;
2724 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
2725 	}
2726 
2727 	override int minHeight() { return defaultLineHeight; }
2728 	override int maxHeight() { return defaultLineHeight * 3/2; }
2729 
2730 	override int minWidth() { return defaultLineHeight * 3/2; }
2731 	override int maxWidth() { return defaultLineHeight * 3/2; }
2732 }
2733 +/
2734 
2735 /+
2736 class DataView : Widget {
2737 	// this is the omnibus data viewer
2738 	// the internal data layout is something like:
2739 	// string[string][] but also each node can have parents
2740 }
2741 +/
2742 
2743 
2744 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
2745 
2746 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
2747 
2748 // FIXME: menus should prolly capture the mouse. ugh i kno.
2749 /*
2750 	TextEdit needs:
2751 
2752 	* caret manipulation
2753 	* selection control
2754 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
2755 
2756 	For example:
2757 
2758 	connect(paste, &textEdit.insertTextAtCaret);
2759 
2760 	would be nice.
2761 
2762 
2763 
2764 	I kinda want an omnibus dataview that combines list, tree,
2765 	and table - it can be switched dynamically between them.
2766 
2767 	Flattening policy: only show top level, show recursive, show grouped
2768 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
2769 
2770 	Single select, multi select, organization, drag+drop
2771 */
2772 
2773 //static if(UsingSimpledisplayX11)
2774 version(win32_widgets) {}
2775 else version(custom_widgets) {
2776 	enum scrollClickRepeatInterval = 50;
2777 
2778 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
2779 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
2780 	enum activeTabColor = lightAccentColor;
2781 	enum hoveringColor = Color(228, 228, 228);
2782 	enum buttonColor = windowBackgroundColor;
2783 	enum depressedButtonColor = darkAccentColor;
2784 	enum activeListXorColor = Color(255, 255, 127);
2785 	enum progressBarColor = Color(0, 0, 128);
2786 	enum activeMenuItemColor = Color(0, 0, 128);
2787 
2788 }}
2789 else static assert(false);
2790 deprecated("Get these properties off the `visualTheme` instead.") {
2791 	// these are used by horizontal rule so not just custom_widgets. for now at least.
2792 	enum darkAccentColor = Color(172, 172, 172);
2793 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
2794 }
2795 
2796 private const(wchar)* toWstringzInternal(in char[] s) {
2797 	wchar[] str;
2798 	str.reserve(s.length + 1);
2799 	foreach(dchar ch; s)
2800 		str ~= ch;
2801 	str ~= '\0';
2802 	return str.ptr;
2803 }
2804 
2805 static if(SimpledisplayTimerAvailable)
2806 void setClickRepeat(Widget w, int interval, int delay = 250) {
2807 	Timer timer;
2808 	int delayRemaining = delay / interval;
2809 	if(delayRemaining <= 1)
2810 		delayRemaining = 2;
2811 
2812 	immutable originalDelayRemaining = delayRemaining;
2813 
2814 	w.addDirectEventListener((scope MouseDownEvent ev) {
2815 		if(ev.srcElement !is w)
2816 			return;
2817 		if(timer !is null) {
2818 			timer.destroy();
2819 			timer = null;
2820 		}
2821 		delayRemaining = originalDelayRemaining;
2822 		timer = new Timer(interval, () {
2823 			if(delayRemaining > 0)
2824 				delayRemaining--;
2825 			else {
2826 				auto ev = new Event("triggered", w);
2827 				ev.sendDirectly();
2828 			}
2829 		});
2830 	});
2831 
2832 	w.addDirectEventListener((scope MouseUpEvent ev) {
2833 		if(ev.srcElement !is w)
2834 			return;
2835 		if(timer !is null) {
2836 			timer.destroy();
2837 			timer = null;
2838 		}
2839 	});
2840 
2841 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
2842 		if(ev.srcElement !is w)
2843 			return;
2844 		if(timer !is null) {
2845 			timer.destroy();
2846 			timer = null;
2847 		}
2848 	});
2849 
2850 }
2851 else
2852 void setClickRepeat(Widget w, int interval, int delay = 250) {}
2853 
2854 enum FrameStyle {
2855 	none, ///
2856 	risen, /// a 3d pop-out effect (think Windows 95 button)
2857 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
2858 	solid, ///
2859 	dotted, ///
2860 	fantasy, /// a style based on a popular fantasy video game
2861 	rounded, /// a rounded rectangle
2862 }
2863 
2864 version(custom_widgets)
2865 deprecated
2866 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
2867 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2868 }
2869 
2870 version(custom_widgets)
2871 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
2872 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
2873 }
2874 
2875 version(custom_widgets)
2876 deprecated
2877 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
2878 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
2879 }
2880 
2881 int getBorderWidth(FrameStyle style) {
2882 	final switch(style) {
2883 		case FrameStyle.sunk, FrameStyle.risen:
2884 			return 2;
2885 		case FrameStyle.none:
2886 			return 0;
2887 		case FrameStyle.solid:
2888 			return 1;
2889 		case FrameStyle.dotted:
2890 			return 1;
2891 		case FrameStyle.fantasy:
2892 			return 3;
2893 		case FrameStyle.rounded:
2894 			return 2;
2895 	}
2896 }
2897 
2898 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
2899 	int borderWidth = getBorderWidth(style);
2900 	final switch(style) {
2901 		case FrameStyle.sunk, FrameStyle.risen:
2902 			// outer layer
2903 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
2904 		break;
2905 		case FrameStyle.none:
2906 			painter.outlineColor = background;
2907 		break;
2908 		case FrameStyle.solid:
2909 		case FrameStyle.rounded:
2910 			painter.pen = Pen(border, 1);
2911 		break;
2912 		case FrameStyle.dotted:
2913 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
2914 		break;
2915 		case FrameStyle.fantasy:
2916 			painter.pen = Pen(border, 3);
2917 		break;
2918 	}
2919 
2920 	painter.fillColor = background;
2921 
2922 	if(style == FrameStyle.rounded) {
2923 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
2924 	} else {
2925 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
2926 
2927 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
2928 			// 3d effect
2929 			auto vt = WidgetPainter.visualTheme;
2930 
2931 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
2932 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
2933 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
2934 
2935 			// inner layer
2936 			//right, bottom
2937 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
2938 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
2939 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
2940 			// left, top
2941 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
2942 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
2943 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
2944 		} else if(style == FrameStyle.fantasy) {
2945 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
2946 			painter.fillColor = Color.transparent;
2947 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
2948 		}
2949 	}
2950 
2951 	return borderWidth;
2952 }
2953 
2954 /++
2955 	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.
2956 
2957 	See_Also:
2958 		[MenuItem]
2959 		[ToolButton]
2960 		[Menu.addItem]
2961 +/
2962 class Action {
2963 	version(win32_widgets) {
2964 		private int id;
2965 		private static int lastId = 9000;
2966 		private static Action[int] mapping;
2967 	}
2968 
2969 	KeyEvent accelerator;
2970 
2971 	// FIXME: disable message
2972 	// and toggle thing?
2973 	// ??? and trigger arguments too ???
2974 
2975 	/++
2976 		Params:
2977 			label = the textual label
2978 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
2979 			triggered = initial handler, more can be added via the [triggered] member.
2980 	+/
2981 	this(string label, ushort icon = 0, void delegate() triggered = null) {
2982 		this.label = label;
2983 		this.iconId = icon;
2984 		if(triggered !is null)
2985 			this.triggered ~= triggered;
2986 		version(win32_widgets) {
2987 			id = ++lastId;
2988 			mapping[id] = this;
2989 		}
2990 	}
2991 
2992 	private string label;
2993 	private ushort iconId;
2994 	// icon
2995 
2996 	// when it is triggered, the triggered event is fired on the window
2997 	/// The list of handlers when it is triggered.
2998 	void delegate()[] triggered;
2999 }
3000 
3001 /*
3002 	plan:
3003 		keyboard accelerators
3004 
3005 		* menus (and popups and tooltips)
3006 		* status bar
3007 		* toolbars and buttons
3008 
3009 		sortable table view
3010 
3011 		maybe notification area icons
3012 		basic clipboard
3013 
3014 		* radio box
3015 		splitter
3016 		toggle buttons (optionally mutually exclusive, like in Paint)
3017 		label, rich text display, multi line plain text (selectable)
3018 		* fieldset
3019 		* nestable grid layout
3020 		single line text input
3021 		* multi line text input
3022 		slider
3023 		spinner
3024 		list box
3025 		drop down
3026 		combo box
3027 		auto complete box
3028 		* progress bar
3029 
3030 		terminal window/widget (on unix it might even be a pty but really idk)
3031 
3032 		ok button
3033 		cancel button
3034 
3035 		keyboard hotkeys
3036 
3037 		scroll widget
3038 
3039 		event redirections and network transparency
3040 		script integration
3041 */
3042 
3043 
3044 /*
3045 	MENUS
3046 
3047 	auto bar = new MenuBar(window);
3048 	window.menuBar = bar;
3049 
3050 	auto fileMenu = bar.addItem(new Menu("&File"));
3051 	fileMenu.addItem(new MenuItem("&Exit"));
3052 
3053 
3054 	EVENTS
3055 
3056 	For controls, you should usually use "triggered" rather than "click", etc., because
3057 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3058 	This is the case on menus and pushbuttons.
3059 
3060 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3061 */
3062 
3063 
3064 /*
3065 enum LinePreference {
3066 	AlwaysOnOwnLine, // always on its own line
3067 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3068 	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
3069 }
3070 */
3071 
3072 /++
3073 	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.
3074 
3075 	---
3076 	class MyWidget : Widget {
3077 		this(Widget parent) { super(parent); }
3078 
3079 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3080 		mixin Padding!q{4};
3081 
3082 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3083 		mixin Margin!q{8};
3084 
3085 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3086 		// while Top/Bottom/Right remain 8 from the mixin above.
3087 		override int marginLeft() { return 2; }
3088 	}
3089 	---
3090 
3091 
3092 	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]).
3093 
3094 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3095 
3096 	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!).
3097 
3098 	* 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.
3099 +/
3100 mixin template Padding(string code) {
3101 	override int paddingLeft() { return mixin(code);}
3102 	override int paddingRight() { return mixin(code);}
3103 	override int paddingTop() { return mixin(code);}
3104 	override int paddingBottom() { return mixin(code);}
3105 }
3106 
3107 /// ditto
3108 mixin template Margin(string code) {
3109 	override int marginLeft() { return mixin(code);}
3110 	override int marginRight() { return mixin(code);}
3111 	override int marginTop() { return mixin(code);}
3112 	override int marginBottom() { return mixin(code);}
3113 }
3114 
3115 private
3116 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3117 	enum calcingV = relevantMeasure == "height";
3118 
3119 	parent.registerMovement();
3120 
3121 	if(parent.children.length == 0)
3122 		return;
3123 
3124 	auto parentStyle = parent.getComputedStyle();
3125 
3126 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3127 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3128 
3129 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3130 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3131 
3132 	// my own width and height should already be set by the caller of this function...
3133 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3134 		mixin("parentStyle.padding"~firstThingy~"()") -
3135 		mixin("parentStyle.padding"~secondThingy~"()");
3136 
3137 	int stretchinessSum;
3138 	int stretchyChildSum;
3139 	int lastMargin = 0;
3140 
3141 	int shrinkinessSum;
3142 	int shrinkyChildSum;
3143 
3144 	// set initial size
3145 	foreach(child; parent.children) {
3146 
3147 		auto childStyle = child.getComputedStyle();
3148 
3149 		if(cast(StaticPosition) child)
3150 			continue;
3151 		if(child.hidden)
3152 			continue;
3153 
3154 		const iw = child.flexBasisWidth();
3155 		const ih = child.flexBasisHeight();
3156 
3157 		static if(calcingV) {
3158 			child.width = parent.width -
3159 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3160 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3161 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3162 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3163 
3164 			if(child.width < 0)
3165 				child.width = 0;
3166 			if(child.width > childStyle.maxWidth())
3167 				child.width = childStyle.maxWidth();
3168 
3169 			if(iw > 0) {
3170 				auto totalPossible = child.width;
3171 				if(child.width > iw && child.widthStretchiness() == 0)
3172 					child.width = iw;
3173 			}
3174 
3175 			child.height = mymax(childStyle.minHeight(), ih);
3176 		} else {
3177 			// set to take all the space
3178 			child.height = parent.height -
3179 				mixin("childStyle.margin"~firstThingy~"()") -
3180 				mixin("childStyle.margin"~secondThingy~"()") -
3181 				mixin("parentStyle.padding"~firstThingy~"()") -
3182 				mixin("parentStyle.padding"~secondThingy~"()");
3183 
3184 			// then clamp it
3185 			if(child.height < 0)
3186 				child.height = 0;
3187 			if(child.height > childStyle.maxHeight())
3188 				child.height = childStyle.maxHeight();
3189 
3190 			// and if possible, respect the ideal target
3191 			if(ih > 0) {
3192 				auto totalPossible = child.height;
3193 				if(child.height > ih && child.heightStretchiness() == 0)
3194 					child.height = ih;
3195 			}
3196 
3197 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3198 			child.width = mymax(childStyle.minWidth(), iw);
3199 		}
3200 
3201 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3202 
3203 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3204 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3205 		lastMargin = margin;
3206 		spaceRemaining -= thisMargin + margin;
3207 
3208 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3209 		stretchinessSum += s;
3210 		if(s > 0)
3211 			stretchyChildSum++;
3212 
3213 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3214 		shrinkinessSum += s2;
3215 		if(s2 > 0)
3216 			shrinkyChildSum++;
3217 	}
3218 
3219 	if(spaceRemaining < 0 && shrinkyChildSum) {
3220 		// shrink to get into the space if it is possible
3221 		auto toRemove = -spaceRemaining;
3222 		auto removalPerItem = toRemove / shrinkinessSum;
3223 		auto remainder = toRemove % shrinkinessSum;
3224 
3225 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3226 
3227 		foreach(child; parent.children) {
3228 			auto childStyle = child.getComputedStyle();
3229 			if(cast(StaticPosition) child)
3230 				continue;
3231 			if(child.hidden)
3232 				continue;
3233 			static if(calcingV) {
3234 				auto minimum = childStyle.minHeight();
3235 				auto stretch = childStyle.heightShrinkiness();
3236 			} else {
3237 				auto minimum = childStyle.minWidth();
3238 				auto stretch = childStyle.widthShrinkiness();
3239 			}
3240 
3241 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3242 				continue;
3243 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3244 
3245 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3246 
3247 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3248 		}
3249 	}
3250 
3251 	// stretch to fill space
3252 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3253 		auto spacePerChild = spaceRemaining / stretchinessSum;
3254 		bool spreadEvenly;
3255 		bool giveToBiggest;
3256 		if(spacePerChild <= 0) {
3257 			spacePerChild = spaceRemaining / stretchyChildSum;
3258 			spreadEvenly = true;
3259 		}
3260 		if(spacePerChild <= 0) {
3261 			giveToBiggest = true;
3262 		}
3263 		int previousSpaceRemaining = spaceRemaining;
3264 		stretchinessSum = 0;
3265 		Widget mostStretchy;
3266 		int mostStretchyS;
3267 		foreach(child; parent.children) {
3268 			auto childStyle = child.getComputedStyle();
3269 			if(cast(StaticPosition) child)
3270 				continue;
3271 			if(child.hidden)
3272 				continue;
3273 			static if(calcingV) {
3274 				auto maximum = childStyle.maxHeight();
3275 			} else {
3276 				auto maximum = childStyle.maxWidth();
3277 			}
3278 
3279 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3280 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3281 				mixin("child._" ~ relevantMeasure) -= adj;
3282 				spaceRemaining += adj;
3283 				continue;
3284 			}
3285 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3286 			if(s <= 0)
3287 				continue;
3288 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3289 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3290 			spaceRemaining -= spaceAdjustment;
3291 			if(mixin("child." ~ relevantMeasure) > maximum) {
3292 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3293 				mixin("child._" ~ relevantMeasure) -= diff;
3294 				spaceRemaining += diff;
3295 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3296 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3297 				if(mostStretchy is null || s >= mostStretchyS) {
3298 					mostStretchy = child;
3299 					mostStretchyS = s;
3300 				}
3301 			}
3302 		}
3303 
3304 		if(giveToBiggest && mostStretchy !is null) {
3305 			auto child = mostStretchy;
3306 			auto childStyle = child.getComputedStyle();
3307 			int spaceAdjustment = spaceRemaining;
3308 
3309 			static if(calcingV)
3310 				auto maximum = childStyle.maxHeight();
3311 			else
3312 				auto maximum = childStyle.maxWidth();
3313 
3314 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3315 			spaceRemaining -= spaceAdjustment;
3316 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3317 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3318 				mixin("child._" ~ relevantMeasure) -= diff;
3319 				spaceRemaining += diff;
3320 			}
3321 		}
3322 
3323 		if(spaceRemaining == previousSpaceRemaining) {
3324 			if(mostStretchy !is null) {
3325 				static if(calcingV)
3326 					auto maximum = mostStretchy.maxHeight();
3327 				else
3328 					auto maximum = mostStretchy.maxWidth();
3329 
3330 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3331 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3332 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3333 			}
3334 			break; // apparently nothing more we can do
3335 		}
3336 	}
3337 
3338 	foreach(child; parent.children) {
3339 		auto childStyle = child.getComputedStyle();
3340 		if(cast(StaticPosition) child)
3341 			continue;
3342 		if(child.hidden)
3343 			continue;
3344 
3345 		static if(calcingV)
3346 			auto maximum = childStyle.maxHeight();
3347 		else
3348 			auto maximum = childStyle.maxWidth();
3349 		if(mixin("child._" ~ relevantMeasure) > maximum)
3350 			mixin("child._" ~ relevantMeasure) = maximum;
3351 	}
3352 
3353 	// position
3354 	lastMargin = 0;
3355 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3356 	foreach(child; parent.children) {
3357 		auto childStyle = child.getComputedStyle();
3358 		if(cast(StaticPosition) child) {
3359 			child.recomputeChildLayout();
3360 			continue;
3361 		}
3362 		if(child.hidden)
3363 			continue;
3364 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3365 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3366 		currentPos += thisMargin;
3367 		static if(calcingV) {
3368 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3369 			child.y = currentPos;
3370 		} else {
3371 			child.x = currentPos;
3372 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3373 
3374 		}
3375 		currentPos += mixin("child." ~ relevantMeasure);
3376 		currentPos += margin;
3377 		lastMargin = margin;
3378 
3379 		child.recomputeChildLayout();
3380 	}
3381 }
3382 
3383 int mymax(int a, int b) { return a > b ? a : b; }
3384 int mymax(int a, int b, int c) {
3385 	auto d = mymax(a, b);
3386 	return c > d ? c : d;
3387 }
3388 
3389 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3390 // and here, it must be integrable with the layout, the event system, and not be painted over.
3391 version(win32_widgets) {
3392 
3393 	// this function just does stuff that a parent window needs for redirection
3394 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3395 		this_.hookedWndProc(msg, wParam, lParam);
3396 
3397 		switch(msg) {
3398 
3399 			case WM_VSCROLL, WM_HSCROLL:
3400 				auto pos = HIWORD(wParam);
3401 				auto m = LOWORD(wParam);
3402 
3403 				auto scrollbarHwnd = cast(HWND) lParam;
3404 
3405 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3406 
3407 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3408 
3409 					switch(m) {
3410 						/+
3411 						// I don't think those messages are ever actually sent normally by the widget itself,
3412 						// they are more used for the keyboard interface. methinks.
3413 						case SB_BOTTOM:
3414 							// writeln("end");
3415 							auto event = new Event("scrolltoend", *widgetp);
3416 							event.dispatch();
3417 							//if(!event.defaultPrevented)
3418 						break;
3419 						case SB_TOP:
3420 							// writeln("top");
3421 							auto event = new Event("scrolltobeginning", *widgetp);
3422 							event.dispatch();
3423 						break;
3424 						case SB_ENDSCROLL:
3425 							// idk
3426 						break;
3427 						+/
3428 						case SB_LINEDOWN:
3429 							(*widgetp).emitCommand!"scrolltonextline"();
3430 						return 0;
3431 						case SB_LINEUP:
3432 							(*widgetp).emitCommand!"scrolltopreviousline"();
3433 						return 0;
3434 						case SB_PAGEDOWN:
3435 							(*widgetp).emitCommand!"scrolltonextpage"();
3436 						return 0;
3437 						case SB_PAGEUP:
3438 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3439 						return 0;
3440 						case SB_THUMBPOSITION:
3441 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3442 							ev.dispatch();
3443 						return 0;
3444 						case SB_THUMBTRACK:
3445 							// eh kinda lying but i like the real time update display
3446 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3447 							ev.dispatch();
3448 
3449 							// the event loop doesn't seem to carry on with a requested redraw..
3450 							// so we request it to get our dirty bit set...
3451 							// then we need to immediately actually redraw it too for instant feedback to user
3452 							SimpleWindow.processAllCustomEvents();
3453 							SimpleWindow.processAllCustomEvents();
3454 							//if(this_.parentWindow)
3455 								//this_.parentWindow.actualRedraw();
3456 
3457 							// and this ensures the WM_PAINT message is sent fairly quickly
3458 							// still seems to lag a little in large windows but meh it basically works.
3459 							if(this_.parentWindow) {
3460 								// FIXME: if painting is slow, this does still lag
3461 								// we probably will want to expose some user hook to ScrollWindowEx
3462 								// or something.
3463 								UpdateWindow(this_.parentWindow.hwnd);
3464 							}
3465 						return 0;
3466 						default:
3467 					}
3468 				}
3469 			break;
3470 
3471 			case WM_CONTEXTMENU:
3472 				auto hwndFrom = cast(HWND) wParam;
3473 
3474 				auto xPos = cast(short) LOWORD(lParam);
3475 				auto yPos = cast(short) HIWORD(lParam);
3476 
3477 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3478 					POINT p;
3479 					p.x = xPos;
3480 					p.y = yPos;
3481 					ScreenToClient(hwnd, &p);
3482 					auto clientX = cast(ushort) p.x;
3483 					auto clientY = cast(ushort) p.y;
3484 
3485 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
3486 
3487 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
3488 						return 0;
3489 					}
3490 				}
3491 			break;
3492 
3493 			case WM_DRAWITEM:
3494 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
3495 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
3496 					return (*widgetp).handleWmDrawItem(dis);
3497 				}
3498 			break;
3499 
3500 			case WM_NOTIFY:
3501 				auto hdr = cast(NMHDR*) lParam;
3502 				auto hwndFrom = hdr.hwndFrom;
3503 				auto code = hdr.code;
3504 
3505 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3506 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
3507 				}
3508 			break;
3509 			case WM_COMMAND:
3510 				auto handle = cast(HWND) lParam;
3511 				auto cmd = HIWORD(wParam);
3512 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
3513 
3514 			default:
3515 				// pass it on
3516 		}
3517 		return 0;
3518 	}
3519 
3520 
3521 
3522 	extern(Windows)
3523 	private
3524 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
3525 	// but can i merge them?!
3526 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3527 		// try { writeln(iMessage); } catch(Exception e) {};
3528 
3529 		if(auto te = hWnd in Widget.nativeMapping) {
3530 			try {
3531 
3532 				te.hookedWndProc(iMessage, wParam, lParam);
3533 
3534 				int mustReturn;
3535 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
3536 				if(mustReturn)
3537 					return ret;
3538 
3539 				if(iMessage == WM_SETFOCUS) {
3540 					auto lol = *te;
3541 					while(lol !is null && lol.implicitlyCreated)
3542 						lol = lol.parent;
3543 					lol.focus();
3544 					//(*te).parentWindow.focusedWidget = lol;
3545 				}
3546 
3547 
3548 				if(iMessage == WM_CTLCOLOREDIT) {
3549 
3550 				}
3551 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
3552 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
3553 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
3554 						//GetStockObject(NULL_BRUSH);
3555 				}
3556 
3557 				auto pos = getChildPositionRelativeToParentOrigin(*te);
3558 				lastDefaultPrevented = false;
3559 				// try { writeln(typeid(*te)); } catch(Exception e) {}
3560 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
3561 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
3562 				else {
3563 					// it was something we recognized, should only call the window procedure if the default was not prevented
3564 				}
3565 			} catch(Exception e) {
3566 				assert(0, e.toString());
3567 			}
3568 			return 0;
3569 		}
3570 		assert(0, "shouldn't be receiving messages for this window....");
3571 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
3572 	}
3573 
3574 	extern(Windows)
3575 	private
3576 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
3577 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3578 		if(iMessage == WM_ERASEBKGND) {
3579 			auto dc = GetDC(hWnd);
3580 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
3581 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
3582 			RECT r;
3583 			GetWindowRect(hWnd, &r);
3584 			// since the pen is null, to fill the whole space, we need the +1 on both.
3585 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
3586 			SelectObject(dc, p);
3587 			SelectObject(dc, b);
3588 			ReleaseDC(hWnd, dc);
3589 			InvalidateRect(hWnd, null, false); // redraw the border
3590 			return 1;
3591 		}
3592 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
3593 	}
3594 
3595 	/++
3596 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
3597 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
3598 		of minigui's expectations.
3599 
3600 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
3601 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
3602 
3603 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
3604 
3605 		To check if you can use this, use `static if(UsingWin32Widgets)`.
3606 	+/
3607 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
3608 		assert(p.parentWindow !is null);
3609 		assert(p.parentWindow.win.impl.hwnd !is null);
3610 
3611 		auto bsgroupbox = style == BS_GROUPBOX;
3612 
3613 		HWND phwnd;
3614 
3615 		auto wtf = p.parent;
3616 		while(wtf) {
3617 			if(wtf.hwnd !is null) {
3618 				phwnd = wtf.hwnd;
3619 				break;
3620 			}
3621 			wtf = wtf.parent;
3622 		}
3623 
3624 		if(phwnd is null)
3625 			phwnd = p.parentWindow.win.impl.hwnd;
3626 
3627 		assert(phwnd !is null);
3628 
3629 		WCharzBuffer wt = WCharzBuffer(windowText);
3630 
3631 		style |= WS_VISIBLE | WS_CHILD;
3632 		//if(className != WC_TABCONTROL)
3633 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
3634 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
3635 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
3636 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
3637 
3638 		assert(p.hwnd !is null);
3639 
3640 
3641 		static HFONT font;
3642 		if(font is null) {
3643 			NONCLIENTMETRICS params;
3644 			params.cbSize = params.sizeof;
3645 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
3646 				font = CreateFontIndirect(&params.lfMessageFont);
3647 			}
3648 		}
3649 
3650 		if(font)
3651 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
3652 
3653 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
3654 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
3655 		Widget.nativeMapping[p.hwnd] = p;
3656 
3657 		if(bsgroupbox)
3658 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
3659 		else
3660 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3661 
3662 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
3663 
3664 		p.registerMovement();
3665 	}
3666 }
3667 
3668 version(win32_widgets)
3669 private
3670 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
3671 	if(hwnd is null || hwnd in Widget.nativeMapping)
3672 		return true;
3673 	auto parent = cast(Widget) cast(void*) lparam;
3674 	Widget p = new Widget(null);
3675 	p._parent = parent;
3676 	p.parentWindow = parent.parentWindow;
3677 	p.hwnd = hwnd;
3678 	p.implicitlyCreated = true;
3679 	Widget.nativeMapping[p.hwnd] = p;
3680 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
3681 	return true;
3682 }
3683 
3684 /++
3685 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
3686 +/
3687 struct WidgetPainter {
3688 	this(ScreenPainter screenPainter, Widget drawingUpon) {
3689 		this.drawingUpon = drawingUpon;
3690 		this.screenPainter = screenPainter;
3691 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3692 			this.screenPainter.setFont(font);
3693 	}
3694 
3695 	/++
3696 		EXPERIMENTAL. subject to change.
3697 
3698 		When you draw a cursor, you can draw this to notify your window of where it is,
3699 		for IME systems to use.
3700 	+/
3701 	void notifyCursorPosition(int x, int y, int width, int height) {
3702 		if(auto a = drawingUpon.parentWindow)
3703 		if(auto w = a.inputProxy) {
3704 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
3705 		}
3706 	}
3707 
3708 
3709 	///
3710 	ScreenPainter screenPainter;
3711 	/// Forward to the screen painter for other methods
3712 	alias screenPainter this;
3713 
3714 	private Widget drawingUpon;
3715 
3716 	/++
3717 		This is the list of rectangles that actually need to be redrawn.
3718 
3719 		Not actually implemented yet.
3720 	+/
3721 	Rectangle[] invalidatedRectangles;
3722 
3723 	private static BaseVisualTheme _visualTheme;
3724 
3725 	/++
3726 		Functions to access the visual theme and helpers to easily use it.
3727 
3728 		These are aware of the current widget's computed style out of the theme.
3729 	+/
3730 	static @property BaseVisualTheme visualTheme() {
3731 		if(_visualTheme is null)
3732 			_visualTheme = new DefaultVisualTheme();
3733 		return _visualTheme;
3734 	}
3735 
3736 	/// ditto
3737 	static @property void visualTheme(BaseVisualTheme theme) {
3738 		_visualTheme = theme;
3739 
3740 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
3741 	}
3742 
3743 	/// ditto
3744 	Color themeForeground() {
3745 		return drawingUpon.getComputedStyle().foregroundColor();
3746 	}
3747 
3748 	/// ditto
3749 	Color themeBackground() {
3750 		return drawingUpon.getComputedStyle().background.color;
3751 	}
3752 
3753 	int isDarkTheme() {
3754 		return 0; // unspecified, yes, no as enum. FIXME
3755 	}
3756 
3757 	/++
3758 		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.
3759 
3760 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
3761 
3762 		If you change teh clip rectangle, you should change it back before you return.
3763 
3764 
3765 		The sequence it uses is:
3766 			background
3767 			content (delegated to you)
3768 			border
3769 			focused outline
3770 			selected overlay
3771 
3772 		Example code:
3773 
3774 		---
3775 		void paint(WidgetPainter painter) {
3776 			painter.drawThemed((bounds) {
3777 				return bounds; // if the selection overlay should be contained, you can return it here.
3778 			});
3779 		}
3780 		---
3781 	+/
3782 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
3783 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
3784 			return drawBody(bounds);
3785 		});
3786 	}
3787 	// this overload is actually mroe for setting the delegate to a virtual function
3788 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
3789 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
3790 
3791 		auto cs = drawingUpon.getComputedStyle();
3792 
3793 		auto bg = cs.background.color;
3794 
3795 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
3796 
3797 		rect.left += borderWidth;
3798 		rect.right -= borderWidth;
3799 		rect.top += borderWidth;
3800 		rect.bottom -= borderWidth;
3801 
3802 		auto insideBorderRect = rect;
3803 
3804 		rect.left += cs.paddingLeft;
3805 		rect.right -= cs.paddingRight;
3806 		rect.top += cs.paddingTop;
3807 		rect.bottom -= cs.paddingBottom;
3808 
3809 		this.outlineColor = this.themeForeground;
3810 		this.fillColor = bg;
3811 
3812 		auto widgetFont = cs.fontCached;
3813 		if(widgetFont !is null)
3814 			this.setFont(widgetFont);
3815 
3816 		rect = drawBody(this, rect);
3817 
3818 		if(widgetFont !is null) {
3819 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
3820 				this.setFont(vtFont);
3821 			else
3822 				this.setFont(null);
3823 		}
3824 
3825 		if(auto os = cs.outlineStyle()) {
3826 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
3827 			this.fillColor = Color.transparent;
3828 			this.drawRectangle(insideBorderRect);
3829 		}
3830 	}
3831 
3832 	/++
3833 		First, draw the background.
3834 		Then draw your content.
3835 		Next, draw the border.
3836 		And the focused indicator.
3837 		And the is-selected box.
3838 
3839 		If it is focused i can draw the outline too...
3840 
3841 		If selected i can even do the xor action but that's at the end.
3842 	+/
3843 	void drawThemeBackground() {
3844 
3845 	}
3846 
3847 	void drawThemeBorder() {
3848 
3849 	}
3850 
3851 	// all this stuff is a dangerous experiment....
3852 	static class ScriptableVersion {
3853 		ScreenPainterImplementation* p;
3854 		int originX, originY;
3855 
3856 		@scriptable:
3857 		void drawRectangle(int x, int y, int width, int height) {
3858 			p.drawRectangle(x + originX, y + originY, width, height);
3859 		}
3860 		void drawLine(int x1, int y1, int x2, int y2) {
3861 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
3862 		}
3863 		void drawText(int x, int y, string text) {
3864 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
3865 		}
3866 		void setOutlineColor(int r, int g, int b) {
3867 			p.pen = Pen(Color(r,g,b), 1);
3868 		}
3869 		void setFillColor(int r, int g, int b) {
3870 			p.fillColor = Color(r,g,b);
3871 		}
3872 	}
3873 
3874 	ScriptableVersion toArsdJsvar() {
3875 		auto sv = new ScriptableVersion;
3876 		sv.p = this.screenPainter.impl;
3877 		sv.originX = this.screenPainter.originX;
3878 		sv.originY = this.screenPainter.originY;
3879 		return sv;
3880 	}
3881 
3882 	static WidgetPainter fromJsVar(T)(T t) {
3883 		return WidgetPainter.init;
3884 	}
3885 	// done..........
3886 }
3887 
3888 
3889 struct Style {
3890 	static struct helper(string m, T) {
3891 		enum method = m;
3892 		T v;
3893 
3894 		mixin template MethodOverride(typeof(this) v) {
3895 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
3896 		}
3897 	}
3898 
3899 	static auto opDispatch(string method, T)(T value) {
3900 		return helper!(method, T)(value);
3901 	}
3902 }
3903 
3904 /++
3905 	Implementation detail of the [ControlledBy] UDA.
3906 
3907 	History:
3908 		Added Oct 28, 2020
3909 +/
3910 struct ControlledBy_(T, Args...) {
3911 	Args args;
3912 
3913 	static if(Args.length)
3914 	this(Args args) {
3915 		this.args = args;
3916 	}
3917 
3918 	private T construct(Widget parent) {
3919 		return new T(args, parent);
3920 	}
3921 }
3922 
3923 /++
3924 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
3925 
3926 	History:
3927 		Added Oct 28, 2020
3928 +/
3929 auto ControlledBy(T, Args...)(Args args) {
3930 	return ControlledBy_!(T, Args)(args);
3931 }
3932 
3933 struct ContainerMeta {
3934 	string name;
3935 	ContainerMeta[] children;
3936 	Widget function(Widget parent) factory;
3937 
3938 	Widget instantiate(Widget parent) {
3939 		auto n = factory(parent);
3940 		n.name = name;
3941 		foreach(child; children)
3942 			child.instantiate(n);
3943 		return n;
3944 	}
3945 }
3946 
3947 /++
3948 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
3949 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
3950 
3951 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
3952 	structures. It works fine on structs declared inside functions though.
3953 
3954 	See: https://issues.dlang.org/show_bug.cgi?id=21984
3955 +/
3956 template Container(CArgs...) {
3957 	static if(CArgs.length && is(CArgs[0] : Widget)) {
3958 		private alias Super = CArgs[0];
3959 		private alias CArgs2 = CArgs[1 .. $];
3960 	} else {
3961 		private alias Super = Layout;
3962 		private alias CArgs2 = CArgs;
3963 	}
3964 
3965 	class Container : Super {
3966 		this(Widget parent) { super(parent); }
3967 
3968 		// just to partially support old gdc versions
3969 		version(GNU) {
3970 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
3971 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
3972 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
3973 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
3974 		} else mixin(q{
3975 			static foreach(Arg; CArgs2) {
3976 				mixin Arg.MethodOverride!(Arg);
3977 			}
3978 		});
3979 
3980 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
3981 			return ContainerMeta(
3982 				name,
3983 				children.dup,
3984 				function (Widget parent) { return new typeof(this)(parent); }
3985 			);
3986 		}
3987 
3988 		static ContainerMeta opCall(ContainerMeta[] children...) {
3989 			return opCall(null, children);
3990 		}
3991 	}
3992 }
3993 
3994 /++
3995 	The data controller widget is created by reflecting over the given
3996 	data type. You can use [ControlledBy] as a UDA on a struct or
3997 	just let it create things automatically.
3998 
3999 	Unlike [dialog], this uses real-time updating of the data and
4000 	you add it to another window yourself.
4001 
4002 	---
4003 		struct Test {
4004 			int x;
4005 			int y;
4006 		}
4007 
4008 		auto window = new Window();
4009 		auto dcw = new DataControllerWidget!Test(new Test, window);
4010 	---
4011 
4012 	The way it works is any public members are given a widget based
4013 	on their data type, and public methods trigger an action button
4014 	if no relevant parameters or a dialog action if it does have
4015 	parameters, similar to the [menu] facility.
4016 
4017 	If you change data programmatically, without going through the
4018 	DataControllerWidget methods, you will have to tell it something
4019 	has changed and it needs to redraw. This is done with the `invalidate`
4020 	method.
4021 
4022 	History:
4023 		Added Oct 28, 2020
4024 +/
4025 /// Group: generating_from_code
4026 class DataControllerWidget(T) : WidgetContainer {
4027 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4028 		private alias Tref = T;
4029 	else
4030 		private alias Tref = T*;
4031 
4032 	Tref datum;
4033 
4034 	/++
4035 		See_also: [addDataControllerWidget]
4036 	+/
4037 	this(Tref datum, Widget parent) {
4038 		this.datum = datum;
4039 
4040 		Widget cp = this;
4041 
4042 		super(parent);
4043 
4044 		foreach(attr; __traits(getAttributes, T))
4045 			static if(is(typeof(attr) == ContainerMeta)) {
4046 				cp = attr.instantiate(this);
4047 			}
4048 
4049 		auto def = this.getByName("default");
4050 		if(def !is null)
4051 			cp = def;
4052 
4053 		Widget helper(string name) {
4054 			auto maybe = this.getByName(name);
4055 			if(maybe is null)
4056 				return cp;
4057 			return maybe;
4058 
4059 		}
4060 
4061 		foreach(member; __traits(allMembers, T))
4062 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4063 		static if(is(typeof(__traits(getMember, this.datum, member))))
4064 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4065 			void delegate() update;
4066 
4067 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4068 
4069 			if(update)
4070 				updaters ~= update;
4071 
4072 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4073 				w.addEventListener("triggered", delegate() {
4074 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4075 					notifyDataUpdated();
4076 				});
4077 			} else static if(is(typeof(w.isChecked) == bool)) {
4078 				w.addEventListener(EventType.change, (Event ev) {
4079 					__traits(getMember, this.datum, member) = w.isChecked;
4080 				});
4081 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4082 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4083 			} else static if(is(typeof(w.value) == int)) {
4084 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4085 			} else static if(is(typeof(w) == DropDownSelection)) {
4086 				// special case for this to kinda support enums and such. coudl be better though
4087 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4088 			} else {
4089 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4090 			}
4091 		}
4092 	}
4093 
4094 	/++
4095 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4096 
4097 		History:
4098 			Added May 28, 2021
4099 	+/
4100 	void notifyDataUpdated() {
4101 		foreach(updater; updaters)
4102 			updater();
4103 
4104 		this.emit!(ChangeEvent!void)(delegate{});
4105 	}
4106 
4107 	private Widget[string] memberWidgets;
4108 	private void delegate()[] updaters;
4109 
4110 	mixin Emits!(ChangeEvent!void);
4111 }
4112 
4113 private int saturatedSum(int[] values...) {
4114 	int sum;
4115 	foreach(value; values) {
4116 		if(value == int.max)
4117 			return int.max;
4118 		sum += value;
4119 	}
4120 	return sum;
4121 }
4122 
4123 void genericSetValue(T, W)(T* where, W what) {
4124 	import std.conv;
4125 	*where = to!T(what);
4126 	//*where = cast(T) stringToLong(what);
4127 }
4128 
4129 /++
4130 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4131 
4132 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4133 
4134 	Note that this creates the widget but does not attach any event handlers to it.
4135 +/
4136 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4137 
4138 	string displayName = __traits(identifier, tt).beautify;
4139 
4140 	static if(controlledByCount!tt == 1) {
4141 		foreach(i, attr; __traits(getAttributes, tt)) {
4142 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4143 				auto w = attr.construct(parent);
4144 				static if(__traits(compiles, w.setPosition(*valptr)))
4145 					update = () { w.setPosition(*valptr); };
4146 				else static if(__traits(compiles, w.setValue(*valptr)))
4147 					update = () { w.setValue(*valptr); };
4148 
4149 				if(update)
4150 					update();
4151 				return w;
4152 			}
4153 		}
4154 	} else static if(controlledByCount!tt == 0) {
4155 		static if(is(typeof(tt) == enum)) {
4156 			// FIXME: update
4157 			auto dds = new DropDownSelection(parent);
4158 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4159 				dds.addOption(option);
4160 				if(__traits(getMember, typeof(tt), option) == *valptr)
4161 					dds.setSelection(cast(int) idx);
4162 			}
4163 			return dds;
4164 		} else static if(is(typeof(tt) == bool)) {
4165 			auto box = new Checkbox(displayName, parent);
4166 			update = () { box.isChecked = *valptr; };
4167 			update();
4168 			return box;
4169 		} else static if(is(typeof(tt) : const long)) {
4170 			auto le = new LabeledLineEdit(displayName, parent);
4171 			update = () { le.content = toInternal!string(*valptr); };
4172 			update();
4173 			return le;
4174 		} else static if(is(typeof(tt) : const double)) {
4175 			auto le = new LabeledLineEdit(displayName, parent);
4176 			import std.conv;
4177 			update = () { le.content = to!string(*valptr); };
4178 			update();
4179 			return le;
4180 		} else static if(is(typeof(tt) : const string)) {
4181 			auto le = new LabeledLineEdit(displayName, parent);
4182 			update = () { le.content = *valptr; };
4183 			update();
4184 			return le;
4185 		} else static if(is(typeof(tt) == function)) {
4186 			auto w = new Button(displayName, parent);
4187 			return w;
4188 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4189 			return parent.addDataControllerWidget(tt);
4190 		} else static assert(0, typeof(tt).stringof);
4191 	} else static assert(0, "multiple controllers not yet supported");
4192 }
4193 
4194 private template controlledByCount(alias tt) {
4195 	static int helper() {
4196 		int count;
4197 		foreach(i, attr; __traits(getAttributes, tt))
4198 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4199 				count++;
4200 		return count;
4201 	}
4202 
4203 	enum controlledByCount = helper;
4204 }
4205 
4206 /++
4207 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4208 
4209 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4210 
4211 	History:
4212 		The `redrawOnChange` parameter was added on May 28, 2021.
4213 +/
4214 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4215 	auto dcw = new DataControllerWidget!T(t, parent);
4216 	initializeDataControllerWidget(dcw, redrawOnChange);
4217 	return dcw;
4218 }
4219 
4220 /// ditto
4221 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4222 	auto dcw = new DataControllerWidget!T(t, parent);
4223 	initializeDataControllerWidget(dcw, redrawOnChange);
4224 	return dcw;
4225 }
4226 
4227 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4228 	if(redrawOnChange !is null)
4229 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4230 }
4231 
4232 /++
4233 	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.
4234 
4235 	History:
4236 		Finalized on June 3, 2021 for the dub v10.0 release
4237 +/
4238 struct StyleInformation {
4239 	private Widget w;
4240 	private BaseVisualTheme visualTheme;
4241 
4242 	private this(Widget w) {
4243 		this.w = w;
4244 		this.visualTheme = WidgetPainter.visualTheme;
4245 	}
4246 
4247 	/++
4248 		Forwards to [Widget.Style]
4249 
4250 		Bugs:
4251 			It is supposed to fall back to the [VisualTheme] if
4252 			the style doesn't override the default, but that is
4253 			not generally implemented. Many of them may end up
4254 			being explicit overloads instead of the generic
4255 			opDispatch fallback, like [font] is now.
4256 	+/
4257 	public @property opDispatch(string name)() {
4258 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4259 		w.useStyleProperties((scope Widget.Style props) {
4260 		//visualTheme.useStyleProperties(w, (props) {
4261 			prop = __traits(getMember, props, name);
4262 		});
4263 		return prop;
4264 	}
4265 
4266 	/++
4267 		Returns the cached font object associated with the widget,
4268 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4269 
4270 		History:
4271 			Prior to March 21, 2022 (dub v10.7), `font` went through
4272 			[opDispatch], which did not use the cache. You can now call it
4273 			repeatedly without guilt.
4274 	+/
4275 	public @property OperatingSystemFont font() {
4276 		OperatingSystemFont prop;
4277 		w.useStyleProperties((scope Widget.Style props) {
4278 			prop = props.fontCached;
4279 		});
4280 		if(prop is null) {
4281 			prop = visualTheme.defaultFontCached(w.currentDpi);
4282 		}
4283 		return prop;
4284 	}
4285 
4286 	@property {
4287 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4288 		/** */ int paddingLeft() { return w.paddingLeft(); }
4289 		/** */ int paddingRight() { return w.paddingRight(); }
4290 		/** */ int paddingTop() { return w.paddingTop(); }
4291 		/** */ int paddingBottom() { return w.paddingBottom(); }
4292 
4293 		/** */ int marginLeft() { return w.marginLeft(); }
4294 		/** */ int marginRight() { return w.marginRight(); }
4295 		/** */ int marginTop() { return w.marginTop(); }
4296 		/** */ int marginBottom() { return w.marginBottom(); }
4297 
4298 		/** */ int maxHeight() { return w.maxHeight(); }
4299 		/** */ int minHeight() { return w.minHeight(); }
4300 
4301 		/** */ int maxWidth() { return w.maxWidth(); }
4302 		/** */ int minWidth() { return w.minWidth(); }
4303 
4304 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4305 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4306 
4307 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4308 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4309 
4310 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4311 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4312 
4313 		// Global helpers some of these are unstable.
4314 		static:
4315 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4316 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4317 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4318 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4319 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4320 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4321 
4322 		/** */ Color activeTabColor() { return lightAccentColor; }
4323 		/** */ Color buttonColor() { return windowBackgroundColor; }
4324 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4325 		/** */ Color hoveringColor() { return lightAccentColor; }
4326 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4327 			auto c = WidgetPainter.visualTheme.selectionColor();
4328 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4329 		}
4330 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4331 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4332 	}
4333 
4334 
4335 
4336 	/+
4337 
4338 	private static auto extractStyleProperty(string name)(Widget w) {
4339 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4340 		w.useStyleProperties((props) {
4341 			prop = __traits(getMember, props, name);
4342 		});
4343 		return prop;
4344 	}
4345 
4346 	// FIXME: clear this upon a X server disconnect
4347 	private static OperatingSystemFont[string] fontCache;
4348 
4349 	T getProperty(T)(string name, lazy T default_) {
4350 		if(visualTheme !is null) {
4351 			auto str = visualTheme.getPropertyString(w, name);
4352 			if(str is null)
4353 				return default_;
4354 			static if(is(T == Color))
4355 				return Color.fromString(str);
4356 			else static if(is(T == Measurement))
4357 				return Measurement(cast(int) toInternal!int(str));
4358 			else static if(is(T == WidgetBackground))
4359 				return WidgetBackground.fromString(str);
4360 			else static if(is(T == OperatingSystemFont)) {
4361 				if(auto f = str in fontCache)
4362 					return *f;
4363 				else
4364 					return fontCache[str] = new OperatingSystemFont(str);
4365 			} else static if(is(T == FrameStyle)) {
4366 				switch(str) {
4367 					default:
4368 						return FrameStyle.none;
4369 					foreach(style; __traits(allMembers, FrameStyle))
4370 					case style:
4371 						return __traits(getMember, FrameStyle, style);
4372 				}
4373 			} else static assert(0);
4374 		} else
4375 			return default_;
4376 	}
4377 
4378 	static struct Measurement {
4379 		int value;
4380 		alias value this;
4381 	}
4382 
4383 	@property:
4384 
4385 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4386 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4387 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4388 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4389 
4390 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4391 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4392 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4393 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4394 
4395 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4396 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4397 
4398 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4399 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4400 
4401 
4402 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4403 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4404 
4405 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4406 
4407 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4408 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4409 
4410 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4411 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4412 
4413 
4414 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4415 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4416 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4417 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4418 
4419 	Color activeTabColor() { return lightAccentColor; }
4420 	Color buttonColor() { return windowBackgroundColor; }
4421 	Color depressedButtonColor() { return darkAccentColor; }
4422 	Color hoveringColor() { return Color(228, 228, 228); }
4423 	Color activeListXorColor() {
4424 		auto c = WidgetPainter.visualTheme.selectionColor();
4425 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4426 	}
4427 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4428 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4429 	+/
4430 }
4431 
4432 
4433 
4434 // pragma(msg, __traits(classInstanceSize, Widget));
4435 
4436 /*private*/ template EventString(E) {
4437 	static if(is(typeof(E.EventString)))
4438 		enum EventString = E.EventString;
4439 	else
4440 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4441 }
4442 
4443 /*private*/ template EventStringIdentifier(E) {
4444 	string helper() {
4445 		auto es = EventString!E;
4446 		char[] id = new char[](es.length * 2);
4447 		size_t idx;
4448 		foreach(char ch; es) {
4449 			id[idx++] = cast(char)('a' + (ch >> 4));
4450 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4451 		}
4452 		return cast(string) id;
4453 	}
4454 
4455 	enum EventStringIdentifier = helper();
4456 }
4457 
4458 
4459 template classStaticallyEmits(This, EventType) {
4460 	static if(is(This Base == super))
4461 		static if(is(Base : Widget))
4462 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4463 		else
4464 			enum baseEmits = false;
4465 	else
4466 		enum baseEmits = false;
4467 
4468 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
4469 
4470 	enum classStaticallyEmits = thisEmits || baseEmits;
4471 }
4472 
4473 /++
4474 	A helper to make widgets out of other native windows.
4475 
4476 	History:
4477 		Factored out of OpenGlWidget on November 5, 2021
4478 +/
4479 class NestedChildWindowWidget : Widget {
4480 	SimpleWindow win;
4481 
4482 	/++
4483 		Used on X to send focus to the appropriate child window when requested by the window manager.
4484 
4485 		Normally returns its own nested window. Can also return another child or null to revert to the parent
4486 		if you override it in a child class.
4487 
4488 		History:
4489 			Added April 2, 2022 (dub v10.8)
4490 	+/
4491 	SimpleWindow focusableWindow() {
4492 		return win;
4493 	}
4494 
4495 	///
4496 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4497 	this(SimpleWindow win, Widget parent) {
4498 		this.parentWindow = parent.parentWindow;
4499 		this.win = win;
4500 
4501 		super(parent);
4502 		windowsetup(win);
4503 	}
4504 
4505 	static protected SimpleWindow getParentWindow(Widget parent) {
4506 		assert(parent !is null);
4507 		SimpleWindow pwin = parent.parentWindow.win;
4508 
4509 		version(win32_widgets) {
4510 			HWND phwnd;
4511 			auto wtf = parent;
4512 			while(wtf) {
4513 				if(wtf.hwnd) {
4514 					phwnd = wtf.hwnd;
4515 					break;
4516 				}
4517 				wtf = wtf.parent;
4518 			}
4519 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
4520 			if(phwnd)
4521 				pwin = new SimpleWindow(phwnd);
4522 		}
4523 
4524 		return pwin;
4525 	}
4526 
4527 	/++
4528 		Called upon the nested window being destroyed.
4529 		Remember the window has already been destroyed at
4530 		this point, so don't use the native handle for anything.
4531 
4532 		History:
4533 			Added April 3, 2022 (dub v10.8)
4534 	+/
4535 	protected void dispose() {
4536 
4537 	}
4538 
4539 	protected void windowsetup(SimpleWindow w) {
4540 		/*
4541 		win.onFocusChange = (bool getting) {
4542 			if(getting)
4543 				this.focus();
4544 		};
4545 		*/
4546 
4547 		/+
4548 		win.onFocusChange = (bool getting) {
4549 			if(getting) {
4550 				this.parentWindow.focusedWidget = this;
4551 				this.emit!FocusEvent();
4552 				this.emit!FocusInEvent();
4553 			} else {
4554 				this.emit!BlurEvent();
4555 				this.emit!FocusOutEvent();
4556 			}
4557 		};
4558 		+/
4559 
4560 		win.onDestroyed = () {
4561 			this.dispose();
4562 		};
4563 
4564 		version(win32_widgets) {
4565 			Widget.nativeMapping[win.hwnd] = this;
4566 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4567 		} else {
4568 			win.setEventHandlers(
4569 				(MouseEvent e) {
4570 					Widget p = this;
4571 					while(p ! is parentWindow) {
4572 						e.x += p.x;
4573 						e.y += p.y;
4574 						p = p.parent;
4575 					}
4576 					parentWindow.dispatchMouseEvent(e);
4577 				},
4578 				(KeyEvent e) {
4579 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
4580 					parentWindow.dispatchKeyEvent(e);
4581 				},
4582 				(dchar e) {
4583 					parentWindow.dispatchCharEvent(e);
4584 				},
4585 			);
4586 		}
4587 
4588 	}
4589 
4590 	override bool showOrHideIfNativeWindow(bool shouldShow) {
4591 		auto cur = hidden;
4592 		win.hidden = !shouldShow;
4593 		if(cur != shouldShow && shouldShow)
4594 			redraw();
4595 		return true;
4596 	}
4597 
4598 	/// OpenGL widgets cannot have child widgets. Do not call this.
4599 	/* @disable */ final override void addChild(Widget, int) {
4600 		throw new Error("cannot add children to OpenGL widgets");
4601 	}
4602 
4603 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
4604 	/// Keep in mind that events like mouse coordinates are still relative to your size.
4605 	override void registerMovement() {
4606 		// writefln("%d %d %d %d", x,y,width,height);
4607 		version(win32_widgets)
4608 			auto pos = getChildPositionRelativeToParentHwnd(this);
4609 		else
4610 			auto pos = getChildPositionRelativeToParentOrigin(this);
4611 		win.moveResize(pos[0], pos[1], width, height);
4612 
4613 		registerMovementAdditionalWork();
4614 		sendResizeEvent();
4615 	}
4616 
4617 	abstract void registerMovementAdditionalWork();
4618 }
4619 
4620 /++
4621 	Nests an opengl capable window inside this window as a widget.
4622 
4623 	You may also just want to create an additional [SimpleWindow] with
4624 	[OpenGlOptions.yes] yourself.
4625 
4626 	An OpenGL widget cannot have child widgets. It will throw if you try.
4627 +/
4628 static if(OpenGlEnabled)
4629 class OpenGlWidget : NestedChildWindowWidget {
4630 
4631 	override void registerMovementAdditionalWork() {
4632 		win.setAsCurrentOpenGlContext();
4633 	}
4634 
4635 	///
4636 	this(Widget parent) {
4637 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4638 		super(win, parent);
4639 	}
4640 
4641 	override void paint(WidgetPainter painter) {
4642 		win.setAsCurrentOpenGlContext();
4643 		glViewport(0, 0, this.width, this.height);
4644 		win.redrawOpenGlSceneNow();
4645 	}
4646 
4647 	void redrawOpenGlScene(void delegate() dg) {
4648 		win.redrawOpenGlScene = dg;
4649 	}
4650 }
4651 
4652 /++
4653 	This demo shows how to draw text in an opengl scene.
4654 +/
4655 unittest {
4656 	import arsd.minigui;
4657 	import arsd.ttf;
4658 
4659 	void main() {
4660 		auto window = new Window();
4661 
4662 		auto widget = new OpenGlWidget(window);
4663 
4664 		// old means non-shader code so compatible with glBegin etc.
4665 		// tbh I haven't implemented new one in font yet...
4666 		// anyway, declaring here, will construct soon.
4667 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
4668 
4669 		// this is a little bit awkward, calling some methods through
4670 		// the underlying SimpleWindow `win` method, and you can't do this
4671 		// on a nanovega widget due to conflicts so I should probably fix
4672 		// the api to be a bit easier. But here it will work.
4673 		//
4674 		// Alternatively, you could load the font on the first draw, inside
4675 		// the redrawOpenGlScene, and keep a flag so you don't do it every
4676 		// time. That'd be a bit easier since the lib sets up the context
4677 		// by then guaranteed.
4678 		//
4679 		// But still, I wanna show this.
4680 		widget.win.visibleForTheFirstTime = delegate {
4681 			// must set the opengl context
4682 			widget.win.setAsCurrentOpenGlContext();
4683 
4684 			// if you were doing a OpenGL 3+ shader, this
4685 			// gets especially important to do in order. With
4686 			// old-style opengl, I think you can even do it
4687 			// in main(), but meh, let's show it more correctly.
4688 
4689 			// Anyway, now it is time to load the font from the
4690 			// OS (you can alternatively load one from a .ttf file
4691 			// you bundle with the application), then load the
4692 			// font into texture for drawing.
4693 
4694 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
4695 
4696 			assert(!osfont.isNull()); // make sure it actually loaded
4697 
4698 			// using typeof to avoid repeating the long name lol
4699 			glfont = new typeof(glfont)(
4700 				// get the raw data from the font for loading in here
4701 				// since it doesn't use the OS function to draw the
4702 				// text, we gotta treat it more as a file than as
4703 				// a drawing api.
4704 				osfont.getTtfBytes(),
4705 				18, // need to respecify size since opengl world is different coordinate system
4706 
4707 				// these last two numbers are why it is called
4708 				// "Limited" font. It only loads the characters
4709 				// in the given range, since the texture atlas
4710 				// it references is all a big image generated ahead
4711 				// of time. You could maybe do the whole thing but
4712 				// idk how much memory that is.
4713 				//
4714 				// But here, 0-128 represents the ASCII range, so
4715 				// good enough for most English things, numeric labels,
4716 				// etc.
4717 				0,
4718 				128
4719 			);
4720 		};
4721 
4722 		widget.redrawOpenGlScene = () {
4723 			// now we can use the glfont's drawString function
4724 
4725 			// first some opengl setup. You can do this in one place
4726 			// on window first visible too in many cases, just showing
4727 			// here cuz it is easier for me.
4728 
4729 			// gonna need some alpha blending or it just looks awful
4730 			glEnable(GL_BLEND);
4731 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
4732 			glClearColor(0,0,0,0);
4733 			glDepthFunc(GL_LEQUAL);
4734 
4735 			// Also need to enable 2d textures, since it draws the
4736 			// font characters as images baked in
4737 			glMatrixMode(GL_MODELVIEW);
4738 			glLoadIdentity();
4739 			glDisable(GL_DEPTH_TEST);
4740 			glEnable(GL_TEXTURE_2D);
4741 
4742 			// the orthographic matrix is best for 2d things like text
4743 			// so let's set that up. This matrix makes the coordinates
4744 			// in the opengl scene be one-to-one with the actual pixels
4745 			// on screen. (Not necessarily best, you may wish to scale
4746 			// things, but it does help keep fonts looking normal.)
4747 			glMatrixMode(GL_PROJECTION);
4748 			glLoadIdentity();
4749 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
4750 
4751 			// you can do other glScale, glRotate, glTranslate, etc
4752 			// to the matrix here of course if you want.
4753 
4754 			// note the x,y coordinates here are for the text baseline
4755 			// NOT the upper-left corner. The baseline is like the line
4756 			// in the notebook you write on. Most the letters are actually
4757 			// above it, but some, like p and q, dip a bit below it.
4758 			//
4759 			// So if you're used to the upper left coordinate like the
4760 			// rest of simpledisplay/minigui usually do, do the
4761 			// y + glfont.ascent to bring it down a little. So this
4762 			// example puts the string in the upper left of the window.
4763 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
4764 
4765 			// re color btw: the function sets a solid color internally,
4766 			// but you actually COULD do your own thing for rainbow effects
4767 			// and the sort if you wanted too, by pulling its guts out.
4768 			// Just view its source for an idea of how it actually draws:
4769 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
4770 
4771 			// it gets a bit complicated with the character positioning,
4772 			// but the opengl parts are fairly simple: bind a texture,
4773 			// set the color, draw a quad for each letter.
4774 
4775 
4776 			// the last optional argument there btw is a bounding box
4777 			// it will/ use to word wrap and return an object you can
4778 			// use to implement scrolling or pagination; it tells how
4779 			// much of the string didn't fit in the box. But for simple
4780 			// labels we can just ignore that.
4781 
4782 
4783 			// I'd suggest drawing text as the last step, after you
4784 			// do your other drawing. You might use the push/pop matrix
4785 			// stuff to keep your place. You, in theory, should be able
4786 			// to do text in a 3d space but I've never actually tried
4787 			// that....
4788 		};
4789 
4790 		window.loop();
4791 	}
4792 }
4793 
4794 version(custom_widgets)
4795 	private alias ListWidgetBase = ScrollableWidget;
4796 else
4797 	private alias ListWidgetBase = Widget;
4798 
4799 /++
4800 	A list widget contains a list of strings that the user can examine and select.
4801 
4802 
4803 	In the future, items in the list may be possible to be more than just strings.
4804 
4805 	See_Also:
4806 		[TableView]
4807 +/
4808 class ListWidget : ListWidgetBase {
4809 	/// 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.
4810 	mixin Emits!(ChangeEvent!void);
4811 
4812 	static struct Option {
4813 		string label;
4814 		bool selected;
4815 		void* tag;
4816 	}
4817 
4818 	/++
4819 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
4820 	+/
4821 	void setSelection(int y) {
4822 		if(!multiSelect)
4823 			foreach(ref opt; options)
4824 				opt.selected = false;
4825 		if(y >= 0 && y < options.length)
4826 			options[y].selected = !options[y].selected;
4827 
4828 		this.emit!(ChangeEvent!void)(delegate {});
4829 
4830 		version(custom_widgets)
4831 			redraw();
4832 	}
4833 
4834 	/++
4835 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
4836 		Returns -1 if nothing is selected.
4837 	+/
4838 	int getSelection()
4839 	{
4840 		foreach(i, opt; options) {
4841 			if (opt.selected)
4842 				return cast(int) i;
4843 		}
4844 		return -1;
4845 	}
4846 
4847 	version(custom_widgets)
4848 	override void defaultEventHandler_click(ClickEvent event) {
4849 		this.focus();
4850 		if(event.button == MouseButton.left) {
4851 			auto y = (event.clientY - 4) / defaultLineHeight;
4852 			if(y >= 0 && y < options.length) {
4853 				setSelection(y);
4854 			}
4855 		}
4856 		super.defaultEventHandler_click(event);
4857 	}
4858 
4859 	this(Widget parent) {
4860 		tabStop = false;
4861 		super(parent);
4862 		version(win32_widgets)
4863 			createWin32Window(this, WC_LISTBOX, "",
4864 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
4865 	}
4866 
4867 	version(win32_widgets)
4868 	override void handleWmCommand(ushort code, ushort id) {
4869 		switch(code) {
4870 			case LBN_SELCHANGE:
4871 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
4872 				setSelection(cast(int) sel);
4873 			break;
4874 			default:
4875 		}
4876 	}
4877 
4878 
4879 	version(custom_widgets)
4880 	override void paintFrameAndBackground(WidgetPainter painter) {
4881 		draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor);
4882 	}
4883 
4884 	version(custom_widgets)
4885 	override void paint(WidgetPainter painter) {
4886 		auto cs = getComputedStyle();
4887 		auto pos = Point(4, 4);
4888 		foreach(idx, option; options) {
4889 			painter.fillColor = painter.visualTheme.widgetBackgroundColor;
4890 			painter.outlineColor = painter.visualTheme.widgetBackgroundColor;
4891 			painter.drawRectangle(pos, width - 8, defaultLineHeight);
4892 			if(option.selected) {
4893 				//painter.rasterOp = RasterOp.xor;
4894 				painter.outlineColor = cs.selectionForegroundColor;
4895 				painter.fillColor = cs.selectionBackgroundColor;
4896 				painter.drawRectangle(pos, width - 8, defaultLineHeight);
4897 				//painter.rasterOp = RasterOp.normal;
4898 			}
4899 			painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor;
4900 			painter.drawText(pos, option.label);
4901 			pos.y += defaultLineHeight;
4902 		}
4903 	}
4904 
4905 	static class Style : Widget.Style {
4906 		override WidgetBackground background() {
4907 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
4908 		}
4909 	}
4910 	mixin OverrideStyle!Style;
4911 	//mixin Padding!q{2};
4912 
4913 	void addOption(string text, void* tag = null) {
4914 		options ~= Option(text, false, tag);
4915 		version(win32_widgets) {
4916 			WCharzBuffer buffer = WCharzBuffer(text);
4917 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
4918 		}
4919 		version(custom_widgets) {
4920 			setContentSize(width, cast(int) (options.length * defaultLineHeight));
4921 			redraw();
4922 		}
4923 	}
4924 
4925 	void clear() {
4926 		options = null;
4927 		version(win32_widgets) {
4928 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
4929 				{}
4930 
4931 		} else version(custom_widgets) {
4932 			scrollTo(Point(0, 0));
4933 			redraw();
4934 		}
4935 	}
4936 
4937 	Option[] options;
4938 	version(win32_widgets)
4939 		enum multiSelect = false; /// not implemented yet
4940 	else
4941 		bool multiSelect;
4942 
4943 	override int heightStretchiness() { return 6; }
4944 }
4945 
4946 
4947 
4948 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
4949 enum ScrollBarShowPolicy {
4950 	automatic, /// automatically show the scroll bar if it is necessary
4951 	never, /// never show the scroll bar (scrolling must be done programmatically)
4952 	always /// always show the scroll bar, even if it is disabled
4953 }
4954 
4955 /++
4956 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
4957 
4958 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
4959 +/
4960 // FIXME ScrollBarShowPolicy
4961 // FIXME: use the ScrollMessageWidget in here now that it exists
4962 class ScrollableWidget : Widget {
4963 	// FIXME: make line size configurable
4964 	// FIXME: add keyboard controls
4965 	version(win32_widgets) {
4966 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
4967 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
4968 				auto pos = HIWORD(wParam);
4969 				auto m = LOWORD(wParam);
4970 
4971 				// FIXME: I can reintroduce the
4972 				// scroll bars now by using this
4973 				// in the top-level window handler
4974 				// to forward comamnds
4975 				auto scrollbarHwnd = lParam;
4976 				switch(m) {
4977 					case SB_BOTTOM:
4978 						if(msg == WM_HSCROLL)
4979 							horizontalScrollTo(contentWidth_);
4980 						else
4981 							verticalScrollTo(contentHeight_);
4982 					break;
4983 					case SB_TOP:
4984 						if(msg == WM_HSCROLL)
4985 							horizontalScrollTo(0);
4986 						else
4987 							verticalScrollTo(0);
4988 					break;
4989 					case SB_ENDSCROLL:
4990 						// idk
4991 					break;
4992 					case SB_LINEDOWN:
4993 						if(msg == WM_HSCROLL)
4994 							horizontalScroll(scaleWithDpi(16));
4995 						else
4996 							verticalScroll(scaleWithDpi(16));
4997 					break;
4998 					case SB_LINEUP:
4999 						if(msg == WM_HSCROLL)
5000 							horizontalScroll(scaleWithDpi(-16));
5001 						else
5002 							verticalScroll(scaleWithDpi(-16));
5003 					break;
5004 					case SB_PAGEDOWN:
5005 						if(msg == WM_HSCROLL)
5006 							horizontalScroll(scaleWithDpi(100));
5007 						else
5008 							verticalScroll(scaleWithDpi(100));
5009 					break;
5010 					case SB_PAGEUP:
5011 						if(msg == WM_HSCROLL)
5012 							horizontalScroll(scaleWithDpi(-100));
5013 						else
5014 							verticalScroll(scaleWithDpi(-100));
5015 					break;
5016 					case SB_THUMBPOSITION:
5017 					case SB_THUMBTRACK:
5018 						if(msg == WM_HSCROLL)
5019 							horizontalScrollTo(pos);
5020 						else
5021 							verticalScrollTo(pos);
5022 
5023 						if(m == SB_THUMBTRACK) {
5024 							// the event loop doesn't seem to carry on with a requested redraw..
5025 							// so we request it to get our dirty bit set...
5026 							redraw();
5027 
5028 							// then we need to immediately actually redraw it too for instant feedback to user
5029 
5030 							SimpleWindow.processAllCustomEvents();
5031 							//if(parentWindow)
5032 								//parentWindow.actualRedraw();
5033 						}
5034 					break;
5035 					default:
5036 				}
5037 			}
5038 			return super.hookedWndProc(msg, wParam, lParam);
5039 		}
5040 	}
5041 	///
5042 	this(Widget parent) {
5043 		this.parentWindow = parent.parentWindow;
5044 
5045 		version(win32_widgets) {
5046 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
5047 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
5048 			super(parent);
5049 		} else version(custom_widgets) {
5050 			outerContainer = new InternalScrollableContainerWidget(this, parent);
5051 			super(outerContainer);
5052 		} else static assert(0);
5053 	}
5054 
5055 	version(custom_widgets)
5056 		InternalScrollableContainerWidget outerContainer;
5057 
5058 	override void defaultEventHandler_click(ClickEvent event) {
5059 		if(event.button == MouseButton.wheelUp)
5060 			verticalScroll(scaleWithDpi(-16));
5061 		if(event.button == MouseButton.wheelDown)
5062 			verticalScroll(scaleWithDpi(16));
5063 		super.defaultEventHandler_click(event);
5064 	}
5065 
5066 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5067 		switch(event.key) {
5068 			case Key.Left:
5069 				horizontalScroll(scaleWithDpi(-16));
5070 			break;
5071 			case Key.Right:
5072 				horizontalScroll(scaleWithDpi(16));
5073 			break;
5074 			case Key.Up:
5075 				verticalScroll(scaleWithDpi(-16));
5076 			break;
5077 			case Key.Down:
5078 				verticalScroll(scaleWithDpi(16));
5079 			break;
5080 			case Key.Home:
5081 				verticalScrollTo(0);
5082 			break;
5083 			case Key.End:
5084 				verticalScrollTo(contentHeight);
5085 			break;
5086 			case Key.PageUp:
5087 				verticalScroll(scaleWithDpi(-160));
5088 			break;
5089 			case Key.PageDown:
5090 				verticalScroll(scaleWithDpi(160));
5091 			break;
5092 			default:
5093 		}
5094 		super.defaultEventHandler_keydown(event);
5095 	}
5096 
5097 
5098 	version(win32_widgets)
5099 	override void recomputeChildLayout() {
5100 		super.recomputeChildLayout();
5101 		SCROLLINFO info;
5102 		info.cbSize = info.sizeof;
5103 		info.nPage = viewportHeight;
5104 		info.fMask = SIF_PAGE | SIF_RANGE;
5105 		info.nMin = 0;
5106 		info.nMax = contentHeight_;
5107 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5108 
5109 		info.cbSize = info.sizeof;
5110 		info.nPage = viewportWidth;
5111 		info.fMask = SIF_PAGE | SIF_RANGE;
5112 		info.nMin = 0;
5113 		info.nMax = contentWidth_;
5114 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5115 	}
5116 
5117 	/*
5118 		Scrolling
5119 		------------
5120 
5121 		You are assigned a width and a height by the layout engine, which
5122 		is your viewport box. However, you may draw more than that by setting
5123 		a contentWidth and contentHeight.
5124 
5125 		If these can be contained by the viewport, no scrollbar is displayed.
5126 		If they cannot fit though, it will automatically show scroll as necessary.
5127 
5128 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5129 		is zero, no vertical scrolling is performed.
5130 
5131 		If scrolling is necessary, the lib will automatically work with the bars.
5132 		When you redraw, the origin and clipping info in the painter is set so if
5133 		you just draw everything, it will work, but you can be more efficient by checking
5134 		the viewportWidth, viewportHeight, and scrollOrigin members.
5135 	*/
5136 
5137 	///
5138 	final @property int viewportWidth() {
5139 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5140 	}
5141 	///
5142 	final @property int viewportHeight() {
5143 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5144 	}
5145 
5146 	// FIXME property
5147 	Point scrollOrigin_;
5148 
5149 	///
5150 	final const(Point) scrollOrigin() {
5151 		return scrollOrigin_;
5152 	}
5153 
5154 	// the user sets these two
5155 	private int contentWidth_ = 0;
5156 	private int contentHeight_ = 0;
5157 
5158 	///
5159 	int contentWidth() { return contentWidth_; }
5160 	///
5161 	int contentHeight() { return contentHeight_; }
5162 
5163 	///
5164 	void setContentSize(int width, int height) {
5165 		contentWidth_ = width;
5166 		contentHeight_ = height;
5167 
5168 		version(custom_widgets) {
5169 			if(showingVerticalScroll || showingHorizontalScroll) {
5170 				outerContainer.queueRecomputeChildLayout();
5171 			}
5172 
5173 			if(showingVerticalScroll())
5174 				outerContainer.verticalScrollBar.redraw();
5175 			if(showingHorizontalScroll())
5176 				outerContainer.horizontalScrollBar.redraw();
5177 		} else version(win32_widgets) {
5178 			queueRecomputeChildLayout();
5179 		} else static assert(0);
5180 	}
5181 
5182 	///
5183 	void verticalScroll(int delta) {
5184 		verticalScrollTo(scrollOrigin.y + delta);
5185 	}
5186 	///
5187 	void verticalScrollTo(int pos) {
5188 		scrollOrigin_.y = pos;
5189 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5190 			scrollOrigin_.y = contentHeight - viewportHeight;
5191 
5192 		if(scrollOrigin_.y < 0)
5193 			scrollOrigin_.y = 0;
5194 
5195 		version(win32_widgets) {
5196 			SCROLLINFO info;
5197 			info.cbSize = info.sizeof;
5198 			info.fMask = SIF_POS;
5199 			info.nPos = scrollOrigin_.y;
5200 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5201 		} else version(custom_widgets) {
5202 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5203 		} else static assert(0);
5204 
5205 		redraw();
5206 	}
5207 
5208 	///
5209 	void horizontalScroll(int delta) {
5210 		horizontalScrollTo(scrollOrigin.x + delta);
5211 	}
5212 	///
5213 	void horizontalScrollTo(int pos) {
5214 		scrollOrigin_.x = pos;
5215 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5216 			scrollOrigin_.x = contentWidth - viewportWidth;
5217 
5218 		if(scrollOrigin_.x < 0)
5219 			scrollOrigin_.x = 0;
5220 
5221 		version(win32_widgets) {
5222 			SCROLLINFO info;
5223 			info.cbSize = info.sizeof;
5224 			info.fMask = SIF_POS;
5225 			info.nPos = scrollOrigin_.x;
5226 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5227 		} else version(custom_widgets) {
5228 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5229 		} else static assert(0);
5230 
5231 		redraw();
5232 	}
5233 	///
5234 	void scrollTo(Point p) {
5235 		verticalScrollTo(p.y);
5236 		horizontalScrollTo(p.x);
5237 	}
5238 
5239 	///
5240 	void ensureVisibleInScroll(Point p) {
5241 		auto rect = viewportRectangle();
5242 		if(rect.contains(p))
5243 			return;
5244 		if(p.x < rect.left)
5245 			horizontalScroll(p.x - rect.left);
5246 		else if(p.x > rect.right)
5247 			horizontalScroll(p.x - rect.right);
5248 
5249 		if(p.y < rect.top)
5250 			verticalScroll(p.y - rect.top);
5251 		else if(p.y > rect.bottom)
5252 			verticalScroll(p.y - rect.bottom);
5253 	}
5254 
5255 	///
5256 	void ensureVisibleInScroll(Rectangle rect) {
5257 		ensureVisibleInScroll(rect.upperLeft);
5258 		ensureVisibleInScroll(rect.lowerRight);
5259 	}
5260 
5261 	///
5262 	Rectangle viewportRectangle() {
5263 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5264 	}
5265 
5266 	///
5267 	bool showingHorizontalScroll() {
5268 		return contentWidth > width;
5269 	}
5270 	///
5271 	bool showingVerticalScroll() {
5272 		return contentHeight > height;
5273 	}
5274 
5275 	/// This is called before the ordinary paint delegate,
5276 	/// giving you a chance to draw the window frame, etc,
5277 	/// before the scroll clip takes effect
5278 	void paintFrameAndBackground(WidgetPainter painter) {
5279 		version(win32_widgets) {
5280 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5281 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5282 			// since the pen is null, to fill the whole space, we need the +1 on both.
5283 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5284 			SelectObject(painter.impl.hdc, p);
5285 			SelectObject(painter.impl.hdc, b);
5286 		}
5287 
5288 	}
5289 
5290 	// make space for the scroll bar, and that's it.
5291 	final override int paddingRight() { return scaleWithDpi(16); }
5292 	final override int paddingBottom() { return scaleWithDpi(16); }
5293 
5294 	/*
5295 		END SCROLLING
5296 	*/
5297 
5298 	override WidgetPainter draw() {
5299 		int x = this.x, y = this.y;
5300 		auto parent = this.parent;
5301 		while(parent) {
5302 			x += parent.x;
5303 			y += parent.y;
5304 			parent = parent.parent;
5305 		}
5306 
5307 		//version(win32_widgets) {
5308 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5309 		//} else {
5310 			auto painter = parentWindow.win.draw(true);
5311 		//}
5312 		painter.originX = x;
5313 		painter.originY = y;
5314 
5315 		painter.originX = painter.originX - scrollOrigin.x;
5316 		painter.originY = painter.originY - scrollOrigin.y;
5317 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
5318 
5319 		return WidgetPainter(painter, this);
5320 	}
5321 
5322 	mixin ScrollableChildren;
5323 }
5324 
5325 // you need to have a Point scrollOrigin in the class somewhere
5326 // and a paintFrameAndBackground
5327 private mixin template ScrollableChildren() {
5328 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5329 		if(hidden)
5330 			return;
5331 
5332 		//version(win32_widgets)
5333 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5334 
5335 		painter.originX = lox + x;
5336 		painter.originY = loy + y;
5337 
5338 		bool actuallyPainted = false;
5339 
5340 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
5341 		if(clip == Rectangle.init)
5342 			return;
5343 
5344 		if(force || redrawRequested) {
5345 			//painter.setClipRectangle(scrollOrigin, width, height);
5346 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5347 			paintFrameAndBackground(painter);
5348 		}
5349 
5350 		/+
5351 		version(win32_widgets) {
5352 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
5353 		}
5354 		+/
5355 
5356 		painter.originX = painter.originX - scrollOrigin.x;
5357 		painter.originY = painter.originY - scrollOrigin.y;
5358 		if(force || redrawRequested) {
5359 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
5360 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
5361 
5362 			//erase(painter); // we paintFrameAndBackground above so no need
5363 			if(painter.visualTheme)
5364 				painter.visualTheme.doPaint(this, painter);
5365 			else
5366 				paint(painter);
5367 
5368 			if(invalidate) {
5369 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5370 				// children are contained inside this, so no need to do extra work
5371 				invalidate = false;
5372 			}
5373 
5374 
5375 			actuallyPainted = true;
5376 			redrawRequested = false;
5377 		}
5378 
5379 		foreach(child; children) {
5380 			if(cast(FixedPosition) child)
5381 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5382 			else
5383 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5384 		}
5385 	}
5386 }
5387 
5388 private class InternalScrollableContainerInsideWidget : ContainerWidget {
5389 	ScrollableContainerWidget scw;
5390 
5391 	this(ScrollableContainerWidget parent) {
5392 		scw = parent;
5393 		super(parent);
5394 	}
5395 
5396 	version(custom_widgets)
5397 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5398 		if(hidden)
5399 			return;
5400 
5401 		bool actuallyPainted = false;
5402 
5403 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
5404 
5405 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
5406 		if(clip == Rectangle.init)
5407 			return;
5408 
5409 		painter.originX = lox + x - scrollOrigin.x;
5410 		painter.originY = loy + y - scrollOrigin.y;
5411 		if(force || redrawRequested) {
5412 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5413 
5414 			erase(painter);
5415 			if(painter.visualTheme)
5416 				painter.visualTheme.doPaint(this, painter);
5417 			else
5418 				paint(painter);
5419 
5420 			if(invalidate) {
5421 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5422 				// children are contained inside this, so no need to do extra work
5423 				invalidate = false;
5424 			}
5425 
5426 			actuallyPainted = true;
5427 			redrawRequested = false;
5428 		}
5429 		foreach(child; children) {
5430 			if(cast(FixedPosition) child)
5431 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5432 			else
5433 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5434 		}
5435 	}
5436 
5437 	version(custom_widgets)
5438 	override protected void addScrollPosition(ref int x, ref int y) {
5439 		x += scw.scrollX_;
5440 		y += scw.scrollY_;
5441 	}
5442 }
5443 
5444 /++
5445 	A widget meant to contain other widgets that may need to scroll.
5446 
5447 	Currently buggy.
5448 
5449 	History:
5450 		Added July 1, 2021 (dub v10.2)
5451 
5452 		On January 3, 2022, I tried to use it in a few other cases
5453 		and found it only worked well in the original test case. Since
5454 		it still sucks, I think I'm going to rewrite it again.
5455 +/
5456 class ScrollableContainerWidget : ContainerWidget {
5457 	///
5458 	this(Widget parent) {
5459 		super(parent);
5460 
5461 		container = new InternalScrollableContainerInsideWidget(this);
5462 		hsb = new HorizontalScrollbar(this);
5463 		vsb = new VerticalScrollbar(this);
5464 
5465 		tabStop = false;
5466 		container.tabStop = false;
5467 		magic = true;
5468 
5469 
5470 		vsb.addEventListener("scrolltonextline", () {
5471 			scrollBy(0, scaleWithDpi(16));
5472 		});
5473 		vsb.addEventListener("scrolltopreviousline", () {
5474 			scrollBy(0,scaleWithDpi( -16));
5475 		});
5476 		vsb.addEventListener("scrolltonextpage", () {
5477 			scrollBy(0, container.height);
5478 		});
5479 		vsb.addEventListener("scrolltopreviouspage", () {
5480 			scrollBy(0, -container.height);
5481 		});
5482 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
5483 			scrollTo(scrollX_, spe.value);
5484 		});
5485 
5486 		this.addEventListener(delegate (scope ClickEvent e) {
5487 			if(e.button == MouseButton.wheelUp) {
5488 				if(!e.defaultPrevented)
5489 					scrollBy(0, scaleWithDpi(-16));
5490 				e.stopPropagation();
5491 			} else if(e.button == MouseButton.wheelDown) {
5492 				if(!e.defaultPrevented)
5493 					scrollBy(0, scaleWithDpi(16));
5494 				e.stopPropagation();
5495 			}
5496 		});
5497 	}
5498 
5499 	/+
5500 	override void defaultEventHandler_click(ClickEvent e) {
5501 	}
5502 	+/
5503 
5504 	override void removeAllChildren() {
5505 		container.removeAllChildren();
5506 	}
5507 
5508 	void scrollTo(int x, int y) {
5509 		scrollBy(x - scrollX_, y - scrollY_);
5510 	}
5511 
5512 	void scrollBy(int x, int y) {
5513 		auto ox = scrollX_;
5514 		auto oy = scrollY_;
5515 
5516 		auto nx = ox + x;
5517 		auto ny = oy + y;
5518 
5519 		if(nx < 0)
5520 			nx = 0;
5521 		if(ny < 0)
5522 			ny = 0;
5523 
5524 		auto maxX = hsb.max - container.width;
5525 		if(maxX < 0) maxX = 0;
5526 		auto maxY = vsb.max - container.height;
5527 		if(maxY < 0) maxY = 0;
5528 
5529 		if(nx > maxX)
5530 			nx = maxX;
5531 		if(ny > maxY)
5532 			ny = maxY;
5533 
5534 		auto dx = nx - ox;
5535 		auto dy = ny - oy;
5536 
5537 		if(dx || dy) {
5538 			version(win32_widgets)
5539 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
5540 			else {
5541 				redraw();
5542 			}
5543 
5544 			hsb.setPosition = nx;
5545 			vsb.setPosition = ny;
5546 
5547 			scrollX_ = nx;
5548 			scrollY_ = ny;
5549 		}
5550 	}
5551 
5552 	private int scrollX_;
5553 	private int scrollY_;
5554 
5555 	void setTotalArea(int width, int height) {
5556 		hsb.setMax(width);
5557 		vsb.setMax(height);
5558 	}
5559 
5560 	///
5561 	void setViewableArea(int width, int height) {
5562 		hsb.setViewableArea(width);
5563 		vsb.setViewableArea(height);
5564 	}
5565 
5566 	private bool magic;
5567 	override void addChild(Widget w, int position = int.max) {
5568 		if(magic)
5569 			container.addChild(w, position);
5570 		else
5571 			super.addChild(w, position);
5572 	}
5573 
5574 	override void recomputeChildLayout() {
5575 		if(hsb is null || vsb is null || container is null) return;
5576 
5577 		/+
5578 		writeln(x, " ", y , " ", width, " ", height);
5579 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
5580 		+/
5581 
5582 		registerMovement();
5583 
5584 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
5585 		hsb.x = 0;
5586 		hsb.y = this.height - hsb.height;
5587 		hsb.width = this.width - scaleWithDpi(16);
5588 		hsb.recomputeChildLayout();
5589 
5590 		vsb.width = scaleWithDpi(16); // FIXME?
5591 		vsb.x = this.width - vsb.width;
5592 		vsb.y = 0;
5593 		vsb.height = this.height - scaleWithDpi(16);
5594 		vsb.recomputeChildLayout();
5595 
5596 		container.x = 0;
5597 		container.y = 0;
5598 		container.width = this.width - vsb.width;
5599 		container.height = this.height - hsb.height;
5600 		container.recomputeChildLayout();
5601 
5602 		scrollX_ = 0;
5603 		scrollY_ = 0;
5604 
5605 		hsb.setPosition(0);
5606 		vsb.setPosition(0);
5607 
5608 		int mw, mh;
5609 		Widget c = container;
5610 		// FIXME: hack here to handle a layout inside...
5611 		if(c.children.length == 1 && cast(Layout) c.children[0])
5612 			c = c.children[0];
5613 		foreach(child; c.children) {
5614 			auto w = child.x + child.width;
5615 			auto h = child.y + child.height;
5616 
5617 			if(w > mw) mw = w;
5618 			if(h > mh) mh = h;
5619 		}
5620 
5621 		setTotalArea(mw, mh);
5622 		setViewableArea(width, height);
5623 	}
5624 
5625 	override int minHeight() { return scaleWithDpi(64); }
5626 
5627 	HorizontalScrollbar hsb;
5628 	VerticalScrollbar vsb;
5629 	ContainerWidget container;
5630 }
5631 
5632 
5633 version(custom_widgets)
5634 private class InternalScrollableContainerWidget : Widget {
5635 
5636 	ScrollableWidget sw;
5637 
5638 	VerticalScrollbar verticalScrollBar;
5639 	HorizontalScrollbar horizontalScrollBar;
5640 
5641 	this(ScrollableWidget sw, Widget parent) {
5642 		this.sw = sw;
5643 
5644 		this.tabStop = false;
5645 
5646 		super(parent);
5647 
5648 		horizontalScrollBar = new HorizontalScrollbar(this);
5649 		verticalScrollBar = new VerticalScrollbar(this);
5650 
5651 		horizontalScrollBar.showing_ = false;
5652 		verticalScrollBar.showing_ = false;
5653 
5654 		horizontalScrollBar.addEventListener("scrolltonextline", {
5655 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
5656 			sw.horizontalScrollTo(horizontalScrollBar.position);
5657 		});
5658 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
5659 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
5660 			sw.horizontalScrollTo(horizontalScrollBar.position);
5661 		});
5662 		verticalScrollBar.addEventListener("scrolltonextline", {
5663 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
5664 			sw.verticalScrollTo(verticalScrollBar.position);
5665 		});
5666 		verticalScrollBar.addEventListener("scrolltopreviousline", {
5667 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
5668 			sw.verticalScrollTo(verticalScrollBar.position);
5669 		});
5670 		horizontalScrollBar.addEventListener("scrolltonextpage", {
5671 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
5672 			sw.horizontalScrollTo(horizontalScrollBar.position);
5673 		});
5674 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
5675 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
5676 			sw.horizontalScrollTo(horizontalScrollBar.position);
5677 		});
5678 		verticalScrollBar.addEventListener("scrolltonextpage", {
5679 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
5680 			sw.verticalScrollTo(verticalScrollBar.position);
5681 		});
5682 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
5683 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
5684 			sw.verticalScrollTo(verticalScrollBar.position);
5685 		});
5686 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
5687 			horizontalScrollBar.setPosition(event.intValue);
5688 			sw.horizontalScrollTo(horizontalScrollBar.position);
5689 		});
5690 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
5691 			verticalScrollBar.setPosition(event.intValue);
5692 			sw.verticalScrollTo(verticalScrollBar.position);
5693 		});
5694 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
5695 			horizontalScrollBar.setPosition(event.intValue);
5696 			sw.horizontalScrollTo(horizontalScrollBar.position);
5697 		});
5698 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
5699 			verticalScrollBar.setPosition(event.intValue);
5700 		});
5701 	}
5702 
5703 	// this is supposed to be basically invisible...
5704 	override int minWidth() { return sw.minWidth; }
5705 	override int minHeight() { return sw.minHeight; }
5706 	override int maxWidth() { return sw.maxWidth; }
5707 	override int maxHeight() { return sw.maxHeight; }
5708 	override int widthStretchiness() { return sw.widthStretchiness; }
5709 	override int heightStretchiness() { return sw.heightStretchiness; }
5710 	override int marginLeft() { return sw.marginLeft; }
5711 	override int marginRight() { return sw.marginRight; }
5712 	override int marginTop() { return sw.marginTop; }
5713 	override int marginBottom() { return sw.marginBottom; }
5714 	override int paddingLeft() { return sw.paddingLeft; }
5715 	override int paddingRight() { return sw.paddingRight; }
5716 	override int paddingTop() { return sw.paddingTop; }
5717 	override int paddingBottom() { return sw.paddingBottom; }
5718 	override void focus() { sw.focus(); }
5719 
5720 
5721 	override void recomputeChildLayout() {
5722 		// The stupid thing needs to calculate if a scroll bar is needed...
5723 		recomputeChildLayoutHelper();
5724 		// then running it again will position things correctly if the bar is NOT needed
5725 		recomputeChildLayoutHelper();
5726 
5727 		// this sucks but meh it barely works
5728 	}
5729 
5730 	private void recomputeChildLayoutHelper() {
5731 		if(sw is null) return;
5732 
5733 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
5734 		if(horizontalScrollBar && verticalScrollBar) {
5735 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
5736 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
5737 			horizontalScrollBar.x = 0;
5738 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
5739 
5740 			verticalScrollBar.width = verticalScrollBar.minWidth();
5741 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
5742 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
5743 			verticalScrollBar.y = 0 + 2;
5744 
5745 			sw.x = 0;
5746 			sw.y = 0;
5747 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
5748 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
5749 
5750 			if(sw.contentWidth_ <= this.width)
5751 				sw.scrollOrigin_.x = 0;
5752 			if(sw.contentHeight_ <= this.height)
5753 				sw.scrollOrigin_.y = 0;
5754 
5755 			horizontalScrollBar.recomputeChildLayout();
5756 			verticalScrollBar.recomputeChildLayout();
5757 			sw.recomputeChildLayout();
5758 		}
5759 
5760 		if(sw.contentWidth_ <= this.width)
5761 			sw.scrollOrigin_.x = 0;
5762 		if(sw.contentHeight_ <= this.height)
5763 			sw.scrollOrigin_.y = 0;
5764 
5765 		if(sw.showingHorizontalScroll())
5766 			horizontalScrollBar.showing(true, false);
5767 		else
5768 			horizontalScrollBar.showing(false, false);
5769 		if(sw.showingVerticalScroll())
5770 			verticalScrollBar.showing(true, false);
5771 		else
5772 			verticalScrollBar.showing(false, false);
5773 
5774 		verticalScrollBar.setViewableArea(sw.viewportHeight());
5775 		verticalScrollBar.setMax(sw.contentHeight);
5776 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
5777 
5778 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
5779 		horizontalScrollBar.setMax(sw.contentWidth);
5780 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
5781 	}
5782 }
5783 
5784 /*
5785 class ScrollableClientWidget : Widget {
5786 	this(Widget parent) {
5787 		super(parent);
5788 	}
5789 	override void paint(WidgetPainter p) {
5790 		parent.paint(p);
5791 	}
5792 }
5793 */
5794 
5795 /++
5796 	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.
5797 +/
5798 abstract class Slider : Widget {
5799 	this(int min, int max, int step, Widget parent) {
5800 		min_ = min;
5801 		max_ = max;
5802 		step_ = step;
5803 		page_ = step;
5804 		super(parent);
5805 	}
5806 
5807 	private int min_;
5808 	private int max_;
5809 	private int step_;
5810 	private int position_;
5811 	private int page_;
5812 
5813 	// selection start and selection end
5814 	// tics
5815 	// tooltip?
5816 	// some way to see and just type the value
5817 	// win32 buddy controls are labels
5818 
5819 	///
5820 	void setMin(int a) {
5821 		min_ = a;
5822 		version(custom_widgets)
5823 			redraw();
5824 		version(win32_widgets)
5825 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
5826 	}
5827 	///
5828 	int min() {
5829 		return min_;
5830 	}
5831 	///
5832 	void setMax(int a) {
5833 		max_ = a;
5834 		version(custom_widgets)
5835 			redraw();
5836 		version(win32_widgets)
5837 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
5838 	}
5839 	///
5840 	int max() {
5841 		return max_;
5842 	}
5843 	///
5844 	void setPosition(int a) {
5845 		if(a > max)
5846 			a = max;
5847 		if(a < min)
5848 			a = min;
5849 		position_ = a;
5850 		version(custom_widgets)
5851 			setPositionCustom(a);
5852 
5853 		version(win32_widgets)
5854 			setPositionWindows(a);
5855 	}
5856 	version(win32_widgets) {
5857 		protected abstract void setPositionWindows(int a);
5858 	}
5859 
5860 	protected abstract int win32direction();
5861 
5862 	/++
5863 		Alias for [position] for better compatibility with generic code.
5864 
5865 		History:
5866 			Added October 5, 2021
5867 	+/
5868 	@property int value() {
5869 		return position;
5870 	}
5871 
5872 	///
5873 	int position() {
5874 		return position_;
5875 	}
5876 	///
5877 	void setStep(int a) {
5878 		step_ = a;
5879 		version(win32_widgets)
5880 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
5881 	}
5882 	///
5883 	int step() {
5884 		return step_;
5885 	}
5886 	///
5887 	void setPageSize(int a) {
5888 		page_ = a;
5889 		version(win32_widgets)
5890 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
5891 	}
5892 	///
5893 	int pageSize() {
5894 		return page_;
5895 	}
5896 
5897 	private void notify() {
5898 		auto event = new ChangeEvent!int(this, &this.position);
5899 		event.dispatch();
5900 	}
5901 
5902 	version(win32_widgets)
5903 	void win32Setup(int style) {
5904 		createWin32Window(this, TRACKBAR_CLASS, "",
5905 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
5906 
5907 		// the trackbar sends the same messages as scroll, which
5908 		// our other layer sends as these... just gonna translate
5909 		// here
5910 		this.addDirectEventListener("scrolltoposition", (Event event) {
5911 			event.stopPropagation();
5912 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
5913 			notify();
5914 		});
5915 		this.addDirectEventListener("scrolltonextline", (Event event) {
5916 			event.stopPropagation();
5917 			this.setPosition(this.position + this.step_ * this.win32direction);
5918 			notify();
5919 		});
5920 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
5921 			event.stopPropagation();
5922 			this.setPosition(this.position - this.step_ * this.win32direction);
5923 			notify();
5924 		});
5925 		this.addDirectEventListener("scrolltonextpage", (Event event) {
5926 			event.stopPropagation();
5927 			this.setPosition(this.position + this.page_ * this.win32direction);
5928 			notify();
5929 		});
5930 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
5931 			event.stopPropagation();
5932 			this.setPosition(this.position - this.page_ * this.win32direction);
5933 			notify();
5934 		});
5935 
5936 		setMin(min_);
5937 		setMax(max_);
5938 		setStep(step_);
5939 		setPageSize(page_);
5940 	}
5941 
5942 	version(custom_widgets) {
5943 		protected MouseTrackingWidget thumb;
5944 
5945 		protected abstract void setPositionCustom(int a);
5946 
5947 		override void defaultEventHandler_keydown(KeyDownEvent event) {
5948 			switch(event.key) {
5949 				case Key.Up:
5950 				case Key.Right:
5951 					setPosition(position() - step() * win32direction);
5952 					changed();
5953 				break;
5954 				case Key.Down:
5955 				case Key.Left:
5956 					setPosition(position() + step() * win32direction);
5957 					changed();
5958 				break;
5959 				case Key.Home:
5960 					setPosition(win32direction > 0 ? min() : max());
5961 					changed();
5962 				break;
5963 				case Key.End:
5964 					setPosition(win32direction > 0 ? max() : min());
5965 					changed();
5966 				break;
5967 				case Key.PageUp:
5968 					setPosition(position() - pageSize() * win32direction);
5969 					changed();
5970 				break;
5971 				case Key.PageDown:
5972 					setPosition(position() + pageSize() * win32direction);
5973 					changed();
5974 				break;
5975 				default:
5976 			}
5977 			super.defaultEventHandler_keydown(event);
5978 		}
5979 
5980 		protected void changed() {
5981 			auto ev = new ChangeEvent!int(this, &position);
5982 			ev.dispatch();
5983 		}
5984 	}
5985 }
5986 
5987 /++
5988 
5989 +/
5990 class VerticalSlider : Slider {
5991 	this(int min, int max, int step, Widget parent) {
5992 		version(custom_widgets)
5993 			initialize();
5994 
5995 		super(min, max, step, parent);
5996 
5997 		version(win32_widgets)
5998 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
5999 	}
6000 
6001 	protected override int win32direction() {
6002 		return -1;
6003 	}
6004 
6005 	version(win32_widgets)
6006 	protected override void setPositionWindows(int a) {
6007 		// the windows thing makes the top 0 and i don't like that.
6008 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
6009 	}
6010 
6011 	version(custom_widgets)
6012 	private void initialize() {
6013 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
6014 
6015 		thumb.tabStop = false;
6016 
6017 		thumb.thumbWidth = width;
6018 		thumb.thumbHeight = scaleWithDpi(16);
6019 
6020 		thumb.addEventListener(EventType.change, () {
6021 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
6022 			sx = max - sx;
6023 			//informProgramThatUserChangedPosition(sx);
6024 
6025 			position_ = sx;
6026 
6027 			changed();
6028 		});
6029 	}
6030 
6031 	version(custom_widgets)
6032 	override void recomputeChildLayout() {
6033 		thumb.thumbWidth = this.width;
6034 		super.recomputeChildLayout();
6035 		setPositionCustom(position_);
6036 	}
6037 
6038 	version(custom_widgets)
6039 	protected override void setPositionCustom(int a) {
6040 		if(max())
6041 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
6042 		redraw();
6043 	}
6044 }
6045 
6046 /++
6047 
6048 +/
6049 class HorizontalSlider : Slider {
6050 	this(int min, int max, int step, Widget parent) {
6051 		version(custom_widgets)
6052 			initialize();
6053 
6054 		super(min, max, step, parent);
6055 
6056 		version(win32_widgets)
6057 			win32Setup(TBS_HORZ);
6058 	}
6059 
6060 	version(win32_widgets)
6061 	protected override void setPositionWindows(int a) {
6062 		SendMessage(hwnd, TBM_SETPOS, true, a);
6063 	}
6064 
6065 	protected override int win32direction() {
6066 		return 1;
6067 	}
6068 
6069 	version(custom_widgets)
6070 	private void initialize() {
6071 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6072 
6073 		thumb.tabStop = false;
6074 
6075 		thumb.thumbWidth = scaleWithDpi(16);
6076 		thumb.thumbHeight = height;
6077 
6078 		thumb.addEventListener(EventType.change, () {
6079 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6080 			//informProgramThatUserChangedPosition(sx);
6081 
6082 			position_ = sx;
6083 
6084 			changed();
6085 		});
6086 	}
6087 
6088 	version(custom_widgets)
6089 	override void recomputeChildLayout() {
6090 		thumb.thumbHeight = this.height;
6091 		super.recomputeChildLayout();
6092 		setPositionCustom(position_);
6093 	}
6094 
6095 	version(custom_widgets)
6096 	protected override void setPositionCustom(int a) {
6097 		if(max())
6098 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6099 		redraw();
6100 	}
6101 }
6102 
6103 
6104 ///
6105 abstract class ScrollbarBase : Widget {
6106 	///
6107 	this(Widget parent) {
6108 		super(parent);
6109 		tabStop = false;
6110 		step_ = scaleWithDpi(16);
6111 	}
6112 
6113 	private int viewableArea_;
6114 	private int max_;
6115 	private int step_;// = 16;
6116 	private int position_;
6117 
6118 	///
6119 	bool atEnd() {
6120 		return position_ + viewableArea_ >= max_;
6121 	}
6122 
6123 	///
6124 	bool atStart() {
6125 		return position_ == 0;
6126 	}
6127 
6128 	///
6129 	void setViewableArea(int a) {
6130 		viewableArea_ = a;
6131 		version(custom_widgets)
6132 			redraw();
6133 	}
6134 	///
6135 	void setMax(int a) {
6136 		max_ = a;
6137 		version(custom_widgets)
6138 			redraw();
6139 	}
6140 	///
6141 	int max() {
6142 		return max_;
6143 	}
6144 	///
6145 	void setPosition(int a) {
6146 		auto logicalMax = max_ - viewableArea_;
6147 		if(a == int.max)
6148 			a = logicalMax;
6149 
6150 		if(a > logicalMax)
6151 			a = logicalMax;
6152 		if(a < 0)
6153 			a = 0;
6154 
6155 		position_ = a;
6156 
6157 		version(custom_widgets)
6158 			redraw();
6159 	}
6160 	///
6161 	int position() {
6162 		return position_;
6163 	}
6164 	///
6165 	void setStep(int a) {
6166 		step_ = a;
6167 	}
6168 	///
6169 	int step() {
6170 		return step_;
6171 	}
6172 
6173 	// FIXME: remove this.... maybe
6174 	/+
6175 	protected void informProgramThatUserChangedPosition(int n) {
6176 		position_ = n;
6177 		auto evt = new Event(EventType.change, this);
6178 		evt.intValue = n;
6179 		evt.dispatch();
6180 	}
6181 	+/
6182 
6183 	version(custom_widgets) {
6184 		enum MIN_THUMB_SIZE = 8;
6185 
6186 		abstract protected int getBarDim();
6187 		int thumbSize() {
6188 			if(viewableArea_ >= max_ || max_ == 0)
6189 				return getBarDim();
6190 
6191 			int res = viewableArea_ * getBarDim() / max_;
6192 
6193 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6194 				res = scaleWithDpi(MIN_THUMB_SIZE);
6195 
6196 			return res;
6197 		}
6198 
6199 		int thumbPosition() {
6200 			/*
6201 				viewableArea_ is the viewport height/width
6202 				position_ is where we are
6203 			*/
6204 			//if(position_ + viewableArea_ >= max_)
6205 				//return getBarDim - thumbSize;
6206 
6207 			auto maximumPossibleValue = getBarDim() - thumbSize;
6208 			auto maximiumLogicalValue = max_ - viewableArea_;
6209 
6210 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6211 
6212 			return p;
6213 		}
6214 	}
6215 }
6216 
6217 //public import mgt;
6218 
6219 /++
6220 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6221 
6222 	Concrete subclasses may include a scrollbar thumb and a volume control.
6223 +/
6224 //version(custom_widgets)
6225 class MouseTrackingWidget : Widget {
6226 
6227 	///
6228 	int positionX() { return positionX_; }
6229 	///
6230 	int positionY() { return positionY_; }
6231 
6232 	///
6233 	void positionX(int p) { positionX_ = p; }
6234 	///
6235 	void positionY(int p) { positionY_ = p; }
6236 
6237 	private int positionX_;
6238 	private int positionY_;
6239 
6240 	///
6241 	enum Orientation {
6242 		horizontal, ///
6243 		vertical, ///
6244 		twoDimensional, ///
6245 	}
6246 
6247 	private int thumbWidth_;
6248 	private int thumbHeight_;
6249 
6250 	///
6251 	int thumbWidth() { return thumbWidth_; }
6252 	///
6253 	int thumbHeight() { return thumbHeight_; }
6254 	///
6255 	int thumbWidth(int a) { return thumbWidth_ = a; }
6256 	///
6257 	int thumbHeight(int a) { return thumbHeight_ = a; }
6258 
6259 	private bool dragging;
6260 	private bool hovering;
6261 	private int startMouseX, startMouseY;
6262 
6263 	///
6264 	this(Orientation orientation, Widget parent) {
6265 		super(parent);
6266 
6267 		//assert(parentWindow !is null);
6268 
6269 		addEventListener((MouseDownEvent event) {
6270 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6271 				dragging = true;
6272 				startMouseX = event.clientX - positionX;
6273 				startMouseY = event.clientY - positionY;
6274 				parentWindow.captureMouse(this);
6275 			} else {
6276 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6277 					positionX = event.clientX - thumbWidth / 2;
6278 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6279 					positionY = event.clientY - thumbHeight / 2;
6280 
6281 				if(positionX + thumbWidth > this.width)
6282 					positionX = this.width - thumbWidth;
6283 				if(positionY + thumbHeight > this.height)
6284 					positionY = this.height - thumbHeight;
6285 
6286 				if(positionX < 0)
6287 					positionX = 0;
6288 				if(positionY < 0)
6289 					positionY = 0;
6290 
6291 
6292 				// this.emit!(ChangeEvent!void)();
6293 				auto evt = new Event(EventType.change, this);
6294 				evt.sendDirectly();
6295 
6296 				redraw();
6297 
6298 			}
6299 		});
6300 
6301 		addEventListener(EventType.mouseup, (Event event) {
6302 			dragging = false;
6303 			parentWindow.releaseMouseCapture();
6304 		});
6305 
6306 		addEventListener(EventType.mouseout, (Event event) {
6307 			if(!hovering)
6308 				return;
6309 			hovering = false;
6310 			redraw();
6311 		});
6312 
6313 		int lpx, lpy;
6314 
6315 		addEventListener((MouseMoveEvent event) {
6316 			auto oh = hovering;
6317 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6318 				hovering = true;
6319 			} else {
6320 				hovering = false;
6321 			}
6322 			if(!dragging) {
6323 				if(hovering != oh)
6324 					redraw();
6325 				return;
6326 			}
6327 
6328 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6329 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
6330 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6331 				positionY = event.clientY - startMouseY;
6332 
6333 			if(positionX + thumbWidth > this.width)
6334 				positionX = this.width - thumbWidth;
6335 			if(positionY + thumbHeight > this.height)
6336 				positionY = this.height - thumbHeight;
6337 
6338 			if(positionX < 0)
6339 				positionX = 0;
6340 			if(positionY < 0)
6341 				positionY = 0;
6342 
6343 			if(positionX != lpx || positionY != lpy) {
6344 				lpx = positionX;
6345 				lpy = positionY;
6346 
6347 				auto evt = new Event(EventType.change, this);
6348 				evt.sendDirectly();
6349 			}
6350 
6351 			redraw();
6352 		});
6353 	}
6354 
6355 	version(custom_widgets)
6356 	override void paint(WidgetPainter painter) {
6357 		auto cs = getComputedStyle();
6358 		auto c = darken(cs.windowBackgroundColor, 0.2);
6359 		painter.outlineColor = c;
6360 		painter.fillColor = c;
6361 		painter.drawRectangle(Point(0, 0), this.width, this.height);
6362 
6363 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
6364 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
6365 	}
6366 }
6367 
6368 //version(custom_widgets)
6369 //private
6370 class HorizontalScrollbar : ScrollbarBase {
6371 
6372 	version(custom_widgets) {
6373 		private MouseTrackingWidget thumb;
6374 
6375 		override int getBarDim() {
6376 			return thumb.width;
6377 		}
6378 	}
6379 
6380 	override void setViewableArea(int a) {
6381 		super.setViewableArea(a);
6382 
6383 		version(win32_widgets) {
6384 			SCROLLINFO info;
6385 			info.cbSize = info.sizeof;
6386 			info.nPage = a + 1;
6387 			info.fMask = SIF_PAGE;
6388 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6389 		} else version(custom_widgets) {
6390 			thumb.positionX = thumbPosition;
6391 			thumb.thumbWidth = thumbSize;
6392 			thumb.redraw();
6393 		} else static assert(0);
6394 
6395 	}
6396 
6397 	override void setMax(int a) {
6398 		super.setMax(a);
6399 		version(win32_widgets) {
6400 			SCROLLINFO info;
6401 			info.cbSize = info.sizeof;
6402 			info.nMin = 0;
6403 			info.nMax = max;
6404 			info.fMask = SIF_RANGE;
6405 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6406 		} else version(custom_widgets) {
6407 			thumb.positionX = thumbPosition;
6408 			thumb.thumbWidth = thumbSize;
6409 			thumb.redraw();
6410 		}
6411 	}
6412 
6413 	override void setPosition(int a) {
6414 		super.setPosition(a);
6415 		version(win32_widgets) {
6416 			SCROLLINFO info;
6417 			info.cbSize = info.sizeof;
6418 			info.fMask = SIF_POS;
6419 			info.nPos = position;
6420 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6421 		} else version(custom_widgets) {
6422 			thumb.positionX = thumbPosition();
6423 			thumb.thumbWidth = thumbSize;
6424 			thumb.redraw();
6425 		} else static assert(0);
6426 	}
6427 
6428 	this(Widget parent) {
6429 		super(parent);
6430 
6431 		version(win32_widgets) {
6432 			createWin32Window(this, "Scrollbar"w, "",
6433 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
6434 		} else version(custom_widgets) {
6435 			auto vl = new HorizontalLayout(this);
6436 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
6437 			leftButton.setClickRepeat(scrollClickRepeatInterval);
6438 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
6439 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
6440 			rightButton.setClickRepeat(scrollClickRepeatInterval);
6441 
6442 			leftButton.tabStop = false;
6443 			rightButton.tabStop = false;
6444 			thumb.tabStop = false;
6445 
6446 			leftButton.addEventListener(EventType.triggered, () {
6447 				this.emitCommand!"scrolltopreviousline"();
6448 				//informProgramThatUserChangedPosition(position - step());
6449 			});
6450 			rightButton.addEventListener(EventType.triggered, () {
6451 				this.emitCommand!"scrolltonextline"();
6452 				//informProgramThatUserChangedPosition(position + step());
6453 			});
6454 
6455 			thumb.thumbWidth = this.minWidth;
6456 			thumb.thumbHeight = scaleWithDpi(16);
6457 
6458 			thumb.addEventListener(EventType.change, () {
6459 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
6460 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
6461 
6462 				//informProgramThatUserChangedPosition(sx);
6463 
6464 				auto ev = new ScrollToPositionEvent(this, sx);
6465 				ev.dispatch();
6466 			});
6467 		}
6468 	}
6469 
6470 	override int minHeight() { return scaleWithDpi(16); }
6471 	override int maxHeight() { return scaleWithDpi(16); }
6472 	override int minWidth() { return scaleWithDpi(48); }
6473 }
6474 
6475 class ScrollToPositionEvent : Event {
6476 	enum EventString = "scrolltoposition";
6477 
6478 	this(Widget target, int value) {
6479 		this.value = value;
6480 		super(EventString, target);
6481 	}
6482 
6483 	immutable int value;
6484 
6485 	override @property int intValue() {
6486 		return value;
6487 	}
6488 }
6489 
6490 //version(custom_widgets)
6491 //private
6492 class VerticalScrollbar : ScrollbarBase {
6493 
6494 	version(custom_widgets) {
6495 		override int getBarDim() {
6496 			return thumb.height;
6497 		}
6498 
6499 		private MouseTrackingWidget thumb;
6500 	}
6501 
6502 	override void setViewableArea(int a) {
6503 		super.setViewableArea(a);
6504 
6505 		version(win32_widgets) {
6506 			SCROLLINFO info;
6507 			info.cbSize = info.sizeof;
6508 			info.nPage = a + 1;
6509 			info.fMask = SIF_PAGE;
6510 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6511 		} else version(custom_widgets) {
6512 			thumb.positionY = thumbPosition;
6513 			thumb.thumbHeight = thumbSize;
6514 			thumb.redraw();
6515 		} else static assert(0);
6516 
6517 	}
6518 
6519 	override void setMax(int a) {
6520 		super.setMax(a);
6521 		version(win32_widgets) {
6522 			SCROLLINFO info;
6523 			info.cbSize = info.sizeof;
6524 			info.nMin = 0;
6525 			info.nMax = max;
6526 			info.fMask = SIF_RANGE;
6527 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6528 		} else version(custom_widgets) {
6529 			thumb.positionY = thumbPosition;
6530 			thumb.thumbHeight = thumbSize;
6531 			thumb.redraw();
6532 		}
6533 	}
6534 
6535 	override void setPosition(int a) {
6536 		super.setPosition(a);
6537 		version(win32_widgets) {
6538 			SCROLLINFO info;
6539 			info.cbSize = info.sizeof;
6540 			info.fMask = SIF_POS;
6541 			info.nPos = position;
6542 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6543 		} else version(custom_widgets) {
6544 			thumb.positionY = thumbPosition;
6545 			thumb.thumbHeight = thumbSize;
6546 			thumb.redraw();
6547 		} else static assert(0);
6548 	}
6549 
6550 	this(Widget parent) {
6551 		super(parent);
6552 
6553 		version(win32_widgets) {
6554 			createWin32Window(this, "Scrollbar"w, "",
6555 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
6556 		} else version(custom_widgets) {
6557 			auto vl = new VerticalLayout(this);
6558 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
6559 			upButton.setClickRepeat(scrollClickRepeatInterval);
6560 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
6561 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
6562 			downButton.setClickRepeat(scrollClickRepeatInterval);
6563 
6564 			upButton.addEventListener(EventType.triggered, () {
6565 				this.emitCommand!"scrolltopreviousline"();
6566 				//informProgramThatUserChangedPosition(position - step());
6567 			});
6568 			downButton.addEventListener(EventType.triggered, () {
6569 				this.emitCommand!"scrolltonextline"();
6570 				//informProgramThatUserChangedPosition(position + step());
6571 			});
6572 
6573 			thumb.thumbWidth = this.minWidth;
6574 			thumb.thumbHeight = scaleWithDpi(16);
6575 
6576 			thumb.addEventListener(EventType.change, () {
6577 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
6578 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
6579 
6580 				auto ev = new ScrollToPositionEvent(this, sy);
6581 				ev.dispatch();
6582 
6583 				//informProgramThatUserChangedPosition(sy);
6584 			});
6585 
6586 			upButton.tabStop = false;
6587 			downButton.tabStop = false;
6588 			thumb.tabStop = false;
6589 		}
6590 	}
6591 
6592 	override int minWidth() { return scaleWithDpi(16); }
6593 	override int maxWidth() { return scaleWithDpi(16); }
6594 	override int minHeight() { return scaleWithDpi(48); }
6595 }
6596 
6597 
6598 /++
6599 	EXPERIMENTAL
6600 
6601 	A widget specialized for being a container for other widgets.
6602 
6603 	History:
6604 		Added May 29, 2021. Not stabilized at this time.
6605 +/
6606 class WidgetContainer : Widget {
6607 	this(Widget parent) {
6608 		tabStop = false;
6609 		super(parent);
6610 	}
6611 
6612 	override int maxHeight() {
6613 		if(this.children.length == 1) {
6614 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
6615 		} else {
6616 			return int.max;
6617 		}
6618 	}
6619 
6620 	override int maxWidth() {
6621 		if(this.children.length == 1) {
6622 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
6623 		} else {
6624 			return int.max;
6625 		}
6626 	}
6627 
6628 	/+
6629 
6630 	override int minHeight() {
6631 		int largest = 0;
6632 		int margins = 0;
6633 		int lastMargin = 0;
6634 		foreach(child; children) {
6635 			auto mh = child.minHeight();
6636 			if(mh > largest)
6637 				largest = mh;
6638 			margins += mymax(lastMargin, child.marginTop());
6639 			lastMargin = child.marginBottom();
6640 		}
6641 		return largest + margins;
6642 	}
6643 
6644 	override int maxHeight() {
6645 		int largest = 0;
6646 		int margins = 0;
6647 		int lastMargin = 0;
6648 		foreach(child; children) {
6649 			auto mh = child.maxHeight();
6650 			if(mh == int.max)
6651 				return int.max;
6652 			if(mh > largest)
6653 				largest = mh;
6654 			margins += mymax(lastMargin, child.marginTop());
6655 			lastMargin = child.marginBottom();
6656 		}
6657 		return largest + margins;
6658 	}
6659 
6660 	override int minWidth() {
6661 		int min;
6662 		foreach(child; children) {
6663 			auto cm = child.minWidth;
6664 			if(cm > min)
6665 				min = cm;
6666 		}
6667 		return min + paddingLeft + paddingRight;
6668 	}
6669 
6670 	override int minHeight() {
6671 		int min;
6672 		foreach(child; children) {
6673 			auto cm = child.minHeight;
6674 			if(cm > min)
6675 				min = cm;
6676 		}
6677 		return min + paddingTop + paddingBottom;
6678 	}
6679 
6680 	override int maxHeight() {
6681 		int largest = 0;
6682 		int margins = 0;
6683 		int lastMargin = 0;
6684 		foreach(child; children) {
6685 			auto mh = child.maxHeight();
6686 			if(mh == int.max)
6687 				return int.max;
6688 			if(mh > largest)
6689 				largest = mh;
6690 			margins += mymax(lastMargin, child.marginTop());
6691 			lastMargin = child.marginBottom();
6692 		}
6693 		return largest + margins;
6694 	}
6695 
6696 	override int heightStretchiness() {
6697 		int max;
6698 		foreach(child; children) {
6699 			auto c = child.heightStretchiness;
6700 			if(c > max)
6701 				max = c;
6702 		}
6703 		return max;
6704 	}
6705 
6706 	override int marginTop() {
6707 		if(this.children.length)
6708 			return this.children[0].marginTop;
6709 		return 0;
6710 	}
6711 	+/
6712 }
6713 
6714 ///
6715 abstract class Layout : Widget {
6716 	this(Widget parent) {
6717 		tabStop = false;
6718 		super(parent);
6719 	}
6720 }
6721 
6722 /++
6723 	Makes all children minimum width and height, placing them down
6724 	left to right, top to bottom.
6725 
6726 	Useful if you want to make a list of buttons that automatically
6727 	wrap to a new line when necessary.
6728 +/
6729 class InlineBlockLayout : Layout {
6730 	///
6731 	this(Widget parent) { super(parent); }
6732 
6733 	override void recomputeChildLayout() {
6734 		registerMovement();
6735 
6736 		int x = this.paddingLeft, y = this.paddingTop;
6737 
6738 		int lineHeight;
6739 		int previousMargin = 0;
6740 		int previousMarginBottom = 0;
6741 
6742 		foreach(child; children) {
6743 			if(child.hidden)
6744 				continue;
6745 			if(cast(FixedPosition) child) {
6746 				child.recomputeChildLayout();
6747 				continue;
6748 			}
6749 			child.width = child.flexBasisWidth();
6750 			if(child.width == 0)
6751 				child.width = child.minWidth();
6752 			if(child.width == 0)
6753 				child.width = 32;
6754 
6755 			child.height = child.flexBasisHeight();
6756 			if(child.height == 0)
6757 				child.height = child.minHeight();
6758 			if(child.height == 0)
6759 				child.height = 32;
6760 
6761 			if(x + child.width + paddingRight > this.width) {
6762 				x = this.paddingLeft;
6763 				y += lineHeight;
6764 				lineHeight = 0;
6765 				previousMargin = 0;
6766 				previousMarginBottom = 0;
6767 			}
6768 
6769 			auto margin = child.marginLeft;
6770 			if(previousMargin > margin)
6771 				margin = previousMargin;
6772 
6773 			x += margin;
6774 
6775 			child.x = x;
6776 			child.y = y;
6777 
6778 			int marginTopApplied;
6779 			if(child.marginTop > previousMarginBottom) {
6780 				child.y += child.marginTop;
6781 				marginTopApplied = child.marginTop;
6782 			}
6783 
6784 			x += child.width;
6785 			previousMargin = child.marginRight;
6786 
6787 			if(child.marginBottom > previousMarginBottom)
6788 				previousMarginBottom = child.marginBottom;
6789 
6790 			auto h = child.height + previousMarginBottom + marginTopApplied;
6791 			if(h > lineHeight)
6792 				lineHeight = h;
6793 
6794 			child.recomputeChildLayout();
6795 		}
6796 
6797 	}
6798 
6799 	override int minWidth() {
6800 		int min;
6801 		foreach(child; children) {
6802 			auto cm = child.minWidth;
6803 			if(cm > min)
6804 				min = cm;
6805 		}
6806 		return min + paddingLeft + paddingRight;
6807 	}
6808 
6809 	override int minHeight() {
6810 		int min;
6811 		foreach(child; children) {
6812 			auto cm = child.minHeight;
6813 			if(cm > min)
6814 				min = cm;
6815 		}
6816 		return min + paddingTop + paddingBottom;
6817 	}
6818 }
6819 
6820 /++
6821 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
6822 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
6823 	the [TabWidget] will automatically change pages of child widgets.
6824 
6825 	This allows you to react to it however you see fit rather than having to
6826 	be tied to just the new sets of child widgets.
6827 
6828 	It sends the message in the form of `this.emitCommand!"changetab"();`.
6829 
6830 	History:
6831 		Added December 24, 2021 (dub v10.5)
6832 +/
6833 class TabMessageWidget : Widget {
6834 
6835 	protected void tabIndexClicked(int item) {
6836 		this.emitCommand!"changetab"();
6837 	}
6838 
6839 	/++
6840 		Adds the a new tab to the control with the given title.
6841 
6842 		Returns:
6843 			The index of the newly added tab. You will need to know
6844 			this index to refer to it later and to know which tab to
6845 			change to when you get a changetab message.
6846 	+/
6847 	int addTab(string title, int pos = int.max) {
6848 		version(win32_widgets) {
6849 			TCITEM item;
6850 			item.mask = TCIF_TEXT;
6851 			WCharzBuffer buf = WCharzBuffer(title);
6852 			item.pszText = buf.ptr;
6853 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
6854 		} else version(custom_widgets) {
6855 			if(pos >= tabs.length) {
6856 				tabs ~= title;
6857 				redraw();
6858 				return cast(int) tabs.length - 1;
6859 			} else if(pos <= 0) {
6860 				tabs = title ~ tabs;
6861 				redraw();
6862 				return 0;
6863 			} else {
6864 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
6865 				redraw();
6866 				return pos;
6867 			}
6868 		}
6869 	}
6870 
6871 	override void addChild(Widget child, int pos = int.max) {
6872 		if(container)
6873 			container.addChild(child, pos);
6874 		else
6875 			super.addChild(child, pos);
6876 	}
6877 
6878 	protected Widget makeContainer() {
6879 		return new Widget(this);
6880 	}
6881 
6882 	private Widget container;
6883 
6884 	override void recomputeChildLayout() {
6885 		version(win32_widgets) {
6886 			this.registerMovement();
6887 
6888 			RECT rect;
6889 			GetWindowRect(hwnd, &rect);
6890 
6891 			auto left = rect.left;
6892 			auto top = rect.top;
6893 
6894 			TabCtrl_AdjustRect(hwnd, false, &rect);
6895 			foreach(child; children) {
6896 				if(!child.showing) continue;
6897 				child.x = rect.left - left;
6898 				child.y = rect.top - top;
6899 				child.width = rect.right - rect.left;
6900 				child.height = rect.bottom - rect.top;
6901 				child.recomputeChildLayout();
6902 			}
6903 		} else version(custom_widgets) {
6904 			this.registerMovement();
6905 			foreach(child; children) {
6906 				if(!child.showing) continue;
6907 				child.x = 2;
6908 				child.y = tabBarHeight + 2; // for the border
6909 				child.width = width - 4; // for the border
6910 				child.height = height - tabBarHeight - 2 - 2; // for the border
6911 				child.recomputeChildLayout();
6912 			}
6913 		} else static assert(0);
6914 	}
6915 
6916 	version(custom_widgets)
6917 		string[] tabs;
6918 
6919 	this(Widget parent) {
6920 		super(parent);
6921 
6922 		tabStop = false;
6923 
6924 		version(win32_widgets) {
6925 			createWin32Window(this, WC_TABCONTROL, "", 0);
6926 		} else version(custom_widgets) {
6927 			addEventListener((ClickEvent event) {
6928 				if(event.target !is this)
6929 					return;
6930 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
6931 					auto t = (event.clientX / tabWidth);
6932 					if(t >= 0 && t < tabs.length) {
6933 						currentTab_ = t;
6934 						tabIndexClicked(t);
6935 						redraw();
6936 					}
6937 				}
6938 			});
6939 		} else static assert(0);
6940 
6941 		this.container = makeContainer();
6942 	}
6943 
6944 	override int marginTop() { return 4; }
6945 	override int paddingBottom() { return 4; }
6946 
6947 	override int minHeight() {
6948 		int max = 0;
6949 		foreach(child; children)
6950 			max = mymax(child.minHeight, max);
6951 
6952 
6953 		version(win32_widgets) {
6954 			RECT rect;
6955 			rect.right = this.width;
6956 			rect.bottom = max;
6957 			TabCtrl_AdjustRect(hwnd, true, &rect);
6958 
6959 			max = rect.bottom;
6960 		} else {
6961 			max += defaultLineHeight + 4;
6962 		}
6963 
6964 
6965 		return max;
6966 	}
6967 
6968 	version(win32_widgets)
6969 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
6970 		switch(code) {
6971 			case TCN_SELCHANGE:
6972 				auto sel = TabCtrl_GetCurSel(hwnd);
6973 				tabIndexClicked(sel);
6974 			break;
6975 			default:
6976 		}
6977 		return 0;
6978 	}
6979 
6980 	version(custom_widgets) {
6981 		private int currentTab_;
6982 		private int tabBarHeight() { return defaultLineHeight; }
6983 		int tabWidth() { return scaleWithDpi(80); }
6984 	}
6985 
6986 	version(win32_widgets)
6987 	override void paint(WidgetPainter painter) {}
6988 
6989 	version(custom_widgets)
6990 	override void paint(WidgetPainter painter) {
6991 		auto cs = getComputedStyle();
6992 
6993 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
6994 
6995 		int posX = 0;
6996 		foreach(idx, title; tabs) {
6997 			auto isCurrent = idx == getCurrentTab();
6998 
6999 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
7000 
7001 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
7002 			painter.outlineColor = cs.foregroundColor;
7003 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
7004 
7005 			if(isCurrent) {
7006 				painter.outlineColor = cs.windowBackgroundColor;
7007 				painter.fillColor = Color.transparent;
7008 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
7009 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
7010 
7011 				painter.outlineColor = Color.white;
7012 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
7013 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
7014 				painter.outlineColor = cs.activeTabColor;
7015 				painter.drawPixel(Point(posX, tabBarHeight - 1));
7016 			}
7017 
7018 			posX += tabWidth - 2;
7019 		}
7020 	}
7021 
7022 	///
7023 	@scriptable
7024 	void setCurrentTab(int item) {
7025 		version(win32_widgets)
7026 			TabCtrl_SetCurSel(hwnd, item);
7027 		else version(custom_widgets)
7028 			currentTab_ = item;
7029 		else static assert(0);
7030 
7031 		tabIndexClicked(item);
7032 	}
7033 
7034 	///
7035 	@scriptable
7036 	int getCurrentTab() {
7037 		version(win32_widgets)
7038 			return TabCtrl_GetCurSel(hwnd);
7039 		else version(custom_widgets)
7040 			return currentTab_; // FIXME
7041 		else static assert(0);
7042 	}
7043 
7044 	///
7045 	@scriptable
7046 	void removeTab(int item) {
7047 		if(item && item == getCurrentTab())
7048 			setCurrentTab(item - 1);
7049 
7050 		version(win32_widgets) {
7051 			TabCtrl_DeleteItem(hwnd, item);
7052 		}
7053 
7054 		for(int a = item; a < children.length - 1; a++)
7055 			this._children[a] = this._children[a + 1];
7056 		this._children = this._children[0 .. $-1];
7057 	}
7058 
7059 }
7060 
7061 
7062 /++
7063 	A tab widget is a set of clickable tab buttons followed by a content area.
7064 
7065 
7066 	Tabs can change existing content or can be new pages.
7067 
7068 	When the user picks a different tab, a `change` message is generated.
7069 +/
7070 class TabWidget : TabMessageWidget {
7071 	this(Widget parent) {
7072 		super(parent);
7073 	}
7074 
7075 	override protected Widget makeContainer() {
7076 		return null;
7077 	}
7078 
7079 	override void addChild(Widget child, int pos = int.max) {
7080 		if(auto twp = cast(TabWidgetPage) child) {
7081 			Widget.addChild(child, pos);
7082 			if(pos == int.max)
7083 				pos = cast(int) this.children.length - 1;
7084 
7085 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7086 
7087 			if(pos != getCurrentTab) {
7088 				child.showing = false;
7089 			}
7090 		} else {
7091 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7092 		}
7093 	}
7094 
7095 	// FIXME: add tab icons at some point, Windows supports them
7096 	/++
7097 		Adds a page and its associated tab with the given label to the widget.
7098 
7099 		Returns:
7100 			The added page object, to which you can add other widgets.
7101 	+/
7102 	@scriptable
7103 	TabWidgetPage addPage(string title) {
7104 		return new TabWidgetPage(title, this);
7105 	}
7106 
7107 	/++
7108 		Gets the page at the given tab index, or `null` if the index is bad.
7109 
7110 		History:
7111 			Added December 24, 2021.
7112 	+/
7113 	TabWidgetPage getPage(int index) {
7114 		if(index < this.children.length)
7115 			return null;
7116 		return cast(TabWidgetPage) this.children[index];
7117 	}
7118 
7119 	/++
7120 		While you can still use the addTab from the parent class,
7121 		*strongly* recommend you use [addPage] insteaad.
7122 
7123 		History:
7124 			Added December 24, 2021 to fulful the interface
7125 			requirement that came from adding [TabMessageWidget].
7126 
7127 			You should not use it though since the [addPage] function
7128 			is much easier to use here.
7129 	+/
7130 	override int addTab(string title, int pos = int.max) {
7131 		auto p = addPage(title);
7132 		foreach(idx, child; this.children)
7133 			if(child is p)
7134 				return cast(int) idx;
7135 		return -1;
7136 	}
7137 
7138 	protected override void tabIndexClicked(int item) {
7139 		foreach(idx, child; children) {
7140 			child.showing(false, false); // batch the recalculates for the end
7141 		}
7142 
7143 		foreach(idx, child; children) {
7144 			if(idx == item) {
7145 				child.showing(true, false);
7146 				if(parentWindow) {
7147 					auto f = parentWindow.getFirstFocusable(child);
7148 					if(f)
7149 						f.focus();
7150 				}
7151 				recomputeChildLayout();
7152 			}
7153 		}
7154 
7155 		version(win32_widgets) {
7156 			InvalidateRect(hwnd, null, true);
7157 		} else version(custom_widgets) {
7158 			this.redraw();
7159 		}
7160 	}
7161 
7162 }
7163 
7164 /++
7165 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7166 
7167 	You add [TabWidgetPage]s to it.
7168 +/
7169 class PageWidget : Widget {
7170 	this(Widget parent) {
7171 		super(parent);
7172 	}
7173 
7174 	override int minHeight() {
7175 		int max = 0;
7176 		foreach(child; children)
7177 			max = mymax(child.minHeight, max);
7178 
7179 		return max;
7180 	}
7181 
7182 
7183 	override void addChild(Widget child, int pos = int.max) {
7184 		if(auto twp = cast(TabWidgetPage) child) {
7185 			super.addChild(child, pos);
7186 			if(pos == int.max)
7187 				pos = cast(int) this.children.length - 1;
7188 
7189 			if(pos != getCurrentTab) {
7190 				child.showing = false;
7191 			}
7192 		} else {
7193 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7194 		}
7195 	}
7196 
7197 	override void recomputeChildLayout() {
7198 		this.registerMovement();
7199 		foreach(child; children) {
7200 			child.x = 0;
7201 			child.y = 0;
7202 			child.width = width;
7203 			child.height = height;
7204 			child.recomputeChildLayout();
7205 		}
7206 	}
7207 
7208 	private int currentTab_;
7209 
7210 	///
7211 	@scriptable
7212 	void setCurrentTab(int item) {
7213 		currentTab_ = item;
7214 
7215 		showOnly(item);
7216 	}
7217 
7218 	///
7219 	@scriptable
7220 	int getCurrentTab() {
7221 		return currentTab_;
7222 	}
7223 
7224 	///
7225 	@scriptable
7226 	void removeTab(int item) {
7227 		if(item && item == getCurrentTab())
7228 			setCurrentTab(item - 1);
7229 
7230 		for(int a = item; a < children.length - 1; a++)
7231 			this._children[a] = this._children[a + 1];
7232 		this._children = this._children[0 .. $-1];
7233 	}
7234 
7235 	///
7236 	@scriptable
7237 	TabWidgetPage addPage(string title) {
7238 		return new TabWidgetPage(title, this);
7239 	}
7240 
7241 	private void showOnly(int item) {
7242 		foreach(idx, child; children)
7243 			if(idx == item) {
7244 				child.show();
7245 				child.queueRecomputeChildLayout();
7246 			} else {
7247 				child.hide();
7248 			}
7249 	}
7250 }
7251 
7252 /++
7253 
7254 +/
7255 class TabWidgetPage : Widget {
7256 	string title;
7257 	this(string title, Widget parent) {
7258 		this.title = title;
7259 		this.tabStop = false;
7260 		super(parent);
7261 
7262 		///*
7263 		version(win32_widgets) {
7264 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7265 		}
7266 		//*/
7267 	}
7268 
7269 	override int minHeight() {
7270 		int sum = 0;
7271 		foreach(child; children)
7272 			sum += child.minHeight();
7273 		return sum;
7274 	}
7275 }
7276 
7277 version(none)
7278 /++
7279 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7280 
7281 	I think I need to modify the layout algorithms to support this.
7282 +/
7283 class CollapsableSidebar : Widget {
7284 
7285 }
7286 
7287 /// Stacks the widgets vertically, taking all the available width for each child.
7288 class VerticalLayout : Layout {
7289 	// most of this is intentionally blank - widget's default is vertical layout right now
7290 	///
7291 	this(Widget parent) { super(parent); }
7292 
7293 	/++
7294 		Sets a max width for the layout so you don't have to subclass. The max width
7295 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7296 
7297 		History:
7298 			Added November 29, 2021 (dub v10.5)
7299 	+/
7300 	this(int maxWidth, Widget parent) {
7301 		this.mw = maxWidth;
7302 		super(parent);
7303 	}
7304 
7305 	private int mw = int.max;
7306 
7307 	override int maxWidth() { return scaleWithDpi(mw); }
7308 }
7309 
7310 /// Stacks the widgets horizontally, taking all the available height for each child.
7311 class HorizontalLayout : Layout {
7312 	///
7313 	this(Widget parent) { super(parent); }
7314 
7315 	/++
7316 		Sets a max height for the layout so you don't have to subclass. The max height
7317 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7318 
7319 		History:
7320 			Added November 29, 2021 (dub v10.5)
7321 	+/
7322 	this(int maxHeight, Widget parent) {
7323 		this.mh = maxHeight;
7324 		super(parent);
7325 	}
7326 
7327 	private int mh = 0;
7328 
7329 
7330 
7331 	override void recomputeChildLayout() {
7332 		.recomputeChildLayout!"width"(this);
7333 	}
7334 
7335 	override int minHeight() {
7336 		int largest = 0;
7337 		int margins = 0;
7338 		int lastMargin = 0;
7339 		foreach(child; children) {
7340 			auto mh = child.minHeight();
7341 			if(mh > largest)
7342 				largest = mh;
7343 			margins += mymax(lastMargin, child.marginTop());
7344 			lastMargin = child.marginBottom();
7345 		}
7346 		return largest + margins;
7347 	}
7348 
7349 	override int maxHeight() {
7350 		if(mh != 0)
7351 			return mymax(minHeight, scaleWithDpi(mh));
7352 
7353 		int largest = 0;
7354 		int margins = 0;
7355 		int lastMargin = 0;
7356 		foreach(child; children) {
7357 			auto mh = child.maxHeight();
7358 			if(mh == int.max)
7359 				return int.max;
7360 			if(mh > largest)
7361 				largest = mh;
7362 			margins += mymax(lastMargin, child.marginTop());
7363 			lastMargin = child.marginBottom();
7364 		}
7365 		return largest + margins;
7366 	}
7367 
7368 	override int heightStretchiness() {
7369 		int max;
7370 		foreach(child; children) {
7371 			auto c = child.heightStretchiness;
7372 			if(c > max)
7373 				max = c;
7374 		}
7375 		return max;
7376 	}
7377 
7378 }
7379 
7380 version(win32_widgets)
7381 private
7382 extern(Windows)
7383 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
7384 	Widget* pwin = hwnd in Widget.nativeMapping;
7385 	if(pwin is null)
7386 		return DefWindowProc(hwnd, message, wparam, lparam);
7387 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
7388 	if(win is null)
7389 		return DefWindowProc(hwnd, message, wparam, lparam);
7390 
7391 	switch(message) {
7392 		case WM_SIZE:
7393 			auto width = LOWORD(lparam);
7394 			auto height = HIWORD(lparam);
7395 
7396 			auto hdc = GetDC(hwnd);
7397 			auto hdcBmp = CreateCompatibleDC(hdc);
7398 
7399 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
7400 			if(width > win.bmpWidth || height > win.bmpHeight) {
7401 				auto oldBuffer = win.buffer;
7402 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
7403 
7404 				if(oldBuffer)
7405 					DeleteObject(oldBuffer);
7406 
7407 				win.bmpWidth = width;
7408 				win.bmpHeight = height;
7409 			}
7410 
7411 			// just always erase it upon resizing so minigui can draw over with a clean slate
7412 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
7413 
7414 			auto brush = GetSysColorBrush(COLOR_3DFACE);
7415 			RECT r;
7416 			r.left = 0;
7417 			r.top = 0;
7418 			r.right = width;
7419 			r.bottom = height;
7420 			FillRect(hdcBmp, &r, brush);
7421 
7422 			SelectObject(hdcBmp, oldBmp);
7423 			DeleteDC(hdcBmp);
7424 			ReleaseDC(hwnd, hdc);
7425 		break;
7426 		case WM_PAINT:
7427 			if(win.buffer is null)
7428 				goto default;
7429 
7430 			BITMAP bm;
7431 			PAINTSTRUCT ps;
7432 
7433 			HDC hdc = BeginPaint(hwnd, &ps);
7434 
7435 			HDC hdcMem = CreateCompatibleDC(hdc);
7436 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
7437 
7438 			GetObject(win.buffer, bm.sizeof, &bm);
7439 
7440 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
7441 
7442 			SelectObject(hdcMem, hbmOld);
7443 			DeleteDC(hdcMem);
7444 			EndPaint(hwnd, &ps);
7445 		break;
7446 		default:
7447 			return DefWindowProc(hwnd, message, wparam, lparam);
7448 	}
7449 
7450 	return 0;
7451 }
7452 
7453 private wstring Win32Class(wstring name)() {
7454 	static bool classRegistered;
7455 	if(!classRegistered) {
7456 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
7457 		WNDCLASSEX wc;
7458 		wc.cbSize = wc.sizeof;
7459 		wc.hInstance = hInstance;
7460 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
7461 		wc.lpfnWndProc = &DoubleBufferWndProc;
7462 		wc.lpszClassName = name.ptr;
7463 		if(!RegisterClassExW(&wc))
7464 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
7465 		classRegistered = true;
7466 	}
7467 
7468 		return name;
7469 }
7470 
7471 /+
7472 version(win32_widgets)
7473 extern(Windows)
7474 private
7475 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
7476 	switch(iMessage) {
7477 		case WM_PAINT:
7478 			if(auto te = hWnd in Widget.nativeMapping) {
7479 				try {
7480 					//te.redraw();
7481 					writeln(te, " drawing");
7482 				} catch(Exception) {}
7483 			}
7484 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7485 		default:
7486 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
7487 	}
7488 }
7489 +/
7490 
7491 
7492 /++
7493 	A widget specifically designed to hold other widgets.
7494 
7495 	History:
7496 		Added July 1, 2021
7497 +/
7498 class ContainerWidget : Widget {
7499 	this(Widget parent) {
7500 		super(parent);
7501 		this.tabStop = false;
7502 
7503 		version(win32_widgets) {
7504 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
7505 		}
7506 	}
7507 }
7508 
7509 /++
7510 	A widget that takes your widget, puts scroll bars around it, and sends
7511 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
7512 	no effort to automatically scroll or clip its child widgets - it just sends
7513 	the messages.
7514 
7515 
7516 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
7517 	The scroll coordinates are all given in a unit you interpret as you wish. One
7518 	of these units is moved on each press of the arrow buttons and represents the
7519 	smallest amount the user can scroll. The intention is for this to be one line,
7520 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
7521 	in each direction that the user might be interested in.
7522 
7523 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
7524 	This is the amount it jumps when the user pressed page up and page down, or clicks
7525 	in the exposed part of the scroll bar.
7526 
7527 	You should add child content to the ScrollMessageWidget. However, it is important to
7528 	note that the coordinates are always independent of the scroll position! It is YOUR
7529 	responsibility to do any necessary transforms, clipping, etc., while drawing the
7530 	content and interpreting mouse events if they are supposed to change with the scroll.
7531 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
7532 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
7533 	you more control (which can be considerably more efficient and adapted to your actual data)
7534 	at the expense of you also needing to be aware of its reality.
7535 
7536 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
7537 	version 10.3. Maybe this will change in the future.... but for now you must call
7538 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
7539 +/
7540 class ScrollMessageWidget : Widget {
7541 	this(Widget parent) {
7542 		super(parent);
7543 
7544 		container = new Widget(this);
7545 		hsb = new HorizontalScrollbar(this);
7546 		vsb = new VerticalScrollbar(this);
7547 
7548 		hsb.addEventListener("scrolltonextline", {
7549 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
7550 			notify();
7551 		});
7552 		hsb.addEventListener("scrolltopreviousline", {
7553 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
7554 			notify();
7555 		});
7556 		vsb.addEventListener("scrolltonextline", {
7557 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
7558 			notify();
7559 		});
7560 		vsb.addEventListener("scrolltopreviousline", {
7561 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
7562 			notify();
7563 		});
7564 		hsb.addEventListener("scrolltonextpage", {
7565 			hsb.setPosition(hsb.position + hsb.step_);
7566 			notify();
7567 		});
7568 		hsb.addEventListener("scrolltopreviouspage", {
7569 			hsb.setPosition(hsb.position - hsb.step_);
7570 			notify();
7571 		});
7572 		vsb.addEventListener("scrolltonextpage", {
7573 			vsb.setPosition(vsb.position + vsb.step_);
7574 			notify();
7575 		});
7576 		vsb.addEventListener("scrolltopreviouspage", {
7577 			vsb.setPosition(vsb.position - vsb.step_);
7578 			notify();
7579 		});
7580 		hsb.addEventListener("scrolltoposition", (Event event) {
7581 			hsb.setPosition(event.intValue);
7582 			notify();
7583 		});
7584 		vsb.addEventListener("scrolltoposition", (Event event) {
7585 			vsb.setPosition(event.intValue);
7586 			notify();
7587 		});
7588 
7589 
7590 		tabStop = false;
7591 		container.tabStop = false;
7592 		magic = true;
7593 	}
7594 
7595 	private int movementPerButtonClickH_ = 1;
7596 	private int movementPerButtonClickV_ = 1;
7597 	public void movementPerButtonClick(int h, int v) {
7598 		movementPerButtonClickH_ = h;
7599 		movementPerButtonClickV_ = v;
7600 	}
7601 
7602 	/++
7603 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
7604 
7605 
7606 		The defaults for [addDefaultWheelListeners] are:
7607 
7608 			$(LIST
7609 				* Mouse wheel scrolls vertically
7610 				* Alt key + mouse wheel scrolls horiontally
7611 				* Shift + mouse wheel scrolls faster.
7612 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
7613 			)
7614 
7615 		The defaults for [addDefaultKeyboardListeners] are:
7616 
7617 			$(LIST
7618 				* Arrow keys scroll by the given amounts
7619 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
7620 				* Page up and down scroll by the vertical viewable area
7621 				* Home and end scroll to the start and end of the verticle viewable area.
7622 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
7623 			)
7624 
7625 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
7626 
7627 		Params:
7628 			horizontalArrowScrollAmount =
7629 			verticalArrowScrollAmount =
7630 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
7631 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
7632 			shiftMultiplier = multiplies the scroll amount by this when shift is held
7633 	+/
7634 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
7635 		auto _this = this;
7636 
7637 		container.addEventListener((scope KeyDownEvent ke) {
7638 			switch(ke.key) {
7639 				case Key.Left:
7640 					_this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7641 				break;
7642 				case Key.Right:
7643 					_this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7644 				break;
7645 				case Key.Up:
7646 					_this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7647 				break;
7648 				case Key.Down:
7649 					_this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1));
7650 				break;
7651 				case Key.PageUp:
7652 					if(ke.altKey)
7653 						_this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7654 					else
7655 						_this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7656 				break;
7657 				case Key.PageDown:
7658 					if(ke.altKey)
7659 						_this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7660 					else
7661 						_this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1));
7662 				break;
7663 				case Key.Home:
7664 					if(ke.altKey)
7665 						_this.scrollLeft(short.max * 16);
7666 					else
7667 						_this.scrollUp(short.max * 16);
7668 				break;
7669 				case Key.End:
7670 					if(ke.altKey)
7671 						_this.scrollRight(short.max * 16);
7672 					else
7673 						_this.scrollDown(short.max * 16);
7674 				break;
7675 
7676 				default:
7677 					// ignore, not for us.
7678 			}
7679 
7680 		});
7681 	}
7682 
7683 	/// ditto
7684 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
7685 		auto _this = this;
7686 		container.addEventListener((scope ClickEvent ce) {
7687 
7688 			//if(ce.target && ce.target.tabStop)
7689 				//ce.target.focus();
7690 
7691 			// ctrl is reserved for the application
7692 			if(ce.ctrlKey)
7693 				return;
7694 
7695 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
7696 				return;
7697 
7698 			if(shiftMultiplier == 0 && ce.shiftKey)
7699 				return;
7700 
7701 			if(ce.button == MouseButton.wheelDown) {
7702 				if(ce.altKey)
7703 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7704 				else
7705 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7706 			} else if(ce.button == MouseButton.wheelUp) {
7707 				if(ce.altKey)
7708 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7709 				else
7710 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
7711 			}
7712 		});
7713 	}
7714 
7715 	/++
7716 		Scrolls the given amount.
7717 
7718 		History:
7719 			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.
7720 	+/
7721 	void scrollUp(int amount = 1) {
7722 		vsb.setPosition(vsb.position - amount);
7723 		notify();
7724 	}
7725 	/// ditto
7726 	void scrollDown(int amount = 1) {
7727 		vsb.setPosition(vsb.position + amount);
7728 		notify();
7729 	}
7730 	/// ditto
7731 	void scrollLeft(int amount = 1) {
7732 		hsb.setPosition(hsb.position - amount);
7733 		notify();
7734 	}
7735 	/// ditto
7736 	void scrollRight(int amount = 1) {
7737 		hsb.setPosition(hsb.position + amount);
7738 		notify();
7739 	}
7740 
7741 	///
7742 	VerticalScrollbar verticalScrollBar() { return vsb; }
7743 	///
7744 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
7745 
7746 	void notify() {
7747 		static bool insideNotify;
7748 
7749 		if(insideNotify)
7750 			return; // avoid the recursive call, even if it isn't strictly correct
7751 
7752 		insideNotify = true;
7753 		scope(exit) insideNotify = false;
7754 
7755 		this.emit!ScrollEvent();
7756 	}
7757 
7758 	mixin Emits!ScrollEvent;
7759 
7760 	///
7761 	Point position() {
7762 		return Point(hsb.position, vsb.position);
7763 	}
7764 
7765 	///
7766 	void setPosition(int x, int y) {
7767 		hsb.setPosition(x);
7768 		vsb.setPosition(y);
7769 	}
7770 
7771 	///
7772 	void setPageSize(int unitsX, int unitsY) {
7773 		hsb.setStep(unitsX);
7774 		vsb.setStep(unitsY);
7775 	}
7776 
7777 	/// Always call this BEFORE setViewableArea
7778 	void setTotalArea(int width, int height) {
7779 		hsb.setMax(width);
7780 		vsb.setMax(height);
7781 	}
7782 
7783 	/++
7784 		Always set the viewable area AFTER setitng the total area if you are going to change both.
7785 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
7786 		If you need to do that, use [queueRecomputeChildLayout].
7787 	+/
7788 	void setViewableArea(int width, int height) {
7789 
7790 		// actually there IS A need to dothis cuz the max might have changed since then
7791 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
7792 			//return; // no need to do what is already done
7793 		hsb.setViewableArea(width);
7794 		vsb.setViewableArea(height);
7795 
7796 		bool needsNotify = false;
7797 
7798 		// FIXME: if at any point the rhs is outside the scrollbar, we need
7799 		// to reset to 0. but it should remember the old position in case the
7800 		// window resizes again, so it can kinda return ot where it was.
7801 		//
7802 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
7803 		if(width >= hsb.max) {
7804 			// there's plenty of room to display it all so we need to reset to zero
7805 			// FIXME: adjust so it matches the note above
7806 			hsb.setPosition(0);
7807 			needsNotify = true;
7808 		}
7809 		if(height >= vsb.max) {
7810 			// there's plenty of room to display it all so we need to reset to zero
7811 			// FIXME: adjust so it matches the note above
7812 			vsb.setPosition(0);
7813 			needsNotify = true;
7814 		}
7815 		if(needsNotify)
7816 			notify();
7817 	}
7818 
7819 	private bool magic;
7820 	override void addChild(Widget w, int position = int.max) {
7821 		if(magic)
7822 			container.addChild(w, position);
7823 		else
7824 			super.addChild(w, position);
7825 	}
7826 
7827 	override void recomputeChildLayout() {
7828 		if(hsb is null || vsb is null || container is null) return;
7829 
7830 		registerMovement();
7831 
7832 		enum BUTTON_SIZE = 16;
7833 
7834 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
7835 		hsb.x = 0;
7836 		hsb.y = this.height - hsb.height;
7837 
7838 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
7839 		vsb.x = this.width - vsb.width;
7840 		vsb.y = 0;
7841 
7842 		auto vsb_width = vsb.showing ? vsb.width : 0;
7843 		auto hsb_height = hsb.showing ? hsb.height : 0;
7844 
7845 		hsb.width = this.width - vsb_width;
7846 		vsb.height = this.height - hsb_height;
7847 
7848 		hsb.recomputeChildLayout();
7849 		vsb.recomputeChildLayout();
7850 
7851 		if(this.header is null) {
7852 			container.x = 0;
7853 			container.y = 0;
7854 			container.width = this.width - vsb_width;
7855 			container.height = this.height - hsb_height;
7856 			container.recomputeChildLayout();
7857 		} else {
7858 			header.x = 0;
7859 			header.y = 0;
7860 			header.width = this.width - vsb_width;
7861 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
7862 			header.recomputeChildLayout();
7863 
7864 			container.x = 0;
7865 			container.y = scaleWithDpi(BUTTON_SIZE);
7866 			container.width = this.width - vsb_width;
7867 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
7868 			container.recomputeChildLayout();
7869 		}
7870 	}
7871 
7872 	private HorizontalScrollbar hsb;
7873 	private VerticalScrollbar vsb;
7874 	Widget container;
7875 	private Widget header;
7876 
7877 	/++
7878 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
7879 
7880 		History:
7881 			Added September 27, 2021 (dub v10.3)
7882 	+/
7883 	Widget getHeader() {
7884 		if(this.header is null) {
7885 			magic = false;
7886 			scope(exit) magic = true;
7887 			this.header = new Widget(this);
7888 			queueRecomputeChildLayout();
7889 		}
7890 		return this.header;
7891 	}
7892 
7893 	/++
7894 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
7895 
7896 		History:
7897 			Added January 3, 2023 (dub v11.0)
7898 	+/
7899 	void scrollIntoView(Rectangle rect) {
7900 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
7901 
7902 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
7903 
7904 		// the lower right is exclusive normally
7905 		auto test = rect.lowerRight;
7906 		if(test.x > 0) test.x--;
7907 		if(test.y > 0) test.y--;
7908 
7909 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
7910 			// try to scroll only one dimension at a time if we can
7911 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
7912 				setPosition(rect.upperLeft.x, position.y);
7913 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
7914 				setPosition(position.x, rect.upperLeft.y);
7915 		}
7916 
7917 	}
7918 
7919 	override int minHeight() {
7920 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
7921 		if(header !is null)
7922 			min += header.minHeight;
7923 		if(horizontalScrollBar.showing)
7924 			min += horizontalScrollBar.minHeight;
7925 		return min;
7926 	}
7927 
7928 	override int maxHeight() {
7929 		int max = container ? container.maxHeight : int.max;
7930 		if(max == int.max)
7931 			return max;
7932 		if(horizontalScrollBar.showing)
7933 			max += horizontalScrollBar.minHeight;
7934 		return max;
7935 	}
7936 }
7937 
7938 /++
7939 	$(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")
7940 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
7941 +/
7942 version(minigui_screenshots)
7943 @Screenshot("ScrollMessageWidget")
7944 unittest {
7945 	auto window = new Window("ScrollMessageWidget");
7946 
7947 	auto smw = new ScrollMessageWidget(window);
7948 	smw.addDefaultKeyboardListeners();
7949 	smw.addDefaultWheelListeners();
7950 
7951 	window.loop();
7952 }
7953 
7954 /++
7955 	Bypasses automatic layout for its children, using manual positioning and sizing only.
7956 	While you need to manually position them, you must ensure they are inside the StaticLayout's
7957 	bounding box to avoid undefined behavior.
7958 
7959 	You should almost never use this.
7960 +/
7961 class StaticLayout : Layout {
7962 	///
7963 	this(Widget parent) { super(parent); }
7964 	override void recomputeChildLayout() {
7965 		registerMovement();
7966 		foreach(child; children)
7967 			child.recomputeChildLayout();
7968 	}
7969 }
7970 
7971 /++
7972 	Bypasses automatic positioning when being laid out. It is your responsibility to make
7973 	room for this widget in the parent layout.
7974 
7975 	Its children are laid out normally, unless there is exactly one, in which case it takes
7976 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
7977 	can do that with `padding`).
7978 +/
7979 class StaticPosition : Layout {
7980 	///
7981 	this(Widget parent) { super(parent); }
7982 
7983 	override void recomputeChildLayout() {
7984 		registerMovement();
7985 		if(this.children.length == 1) {
7986 			auto child = children[0];
7987 			child.x = 0;
7988 			child.y = 0;
7989 			child.width = this.width;
7990 			child.height = this.height;
7991 			child.recomputeChildLayout();
7992 		} else
7993 		foreach(child; children)
7994 			child.recomputeChildLayout();
7995 	}
7996 
7997 	alias width = typeof(super).width;
7998 	alias height = typeof(super).height;
7999 
8000 	@property int width(int w) @nogc pure @safe nothrow {
8001 		return this._width = w;
8002 	}
8003 
8004 	@property int height(int w) @nogc pure @safe nothrow {
8005 		return this._height = w;
8006 	}
8007 
8008 }
8009 
8010 /++
8011 	FixedPosition is like [StaticPosition], but its coordinates
8012 	are always relative to the viewport, meaning they do not scroll with
8013 	the parent content.
8014 +/
8015 class FixedPosition : StaticPosition {
8016 	///
8017 	this(Widget parent) { super(parent); }
8018 }
8019 
8020 version(win32_widgets)
8021 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
8022 	if(true) {
8023 		// cmd == 0 = menu, cmd == 1 = accelerator
8024 		if(auto item = idm in Action.mapping) {
8025 			foreach(handler; (*item).triggered)
8026 				handler();
8027 		/*
8028 			auto event = new Event("triggered", *item);
8029 			event.button = idm;
8030 			event.dispatch();
8031 		*/
8032 			return 0;
8033 		}
8034 	}
8035 	if(handle)
8036 	if(auto widgetp = handle in Widget.nativeMapping) {
8037 		(*widgetp).handleWmCommand(cmd, idm);
8038 		return 0;
8039 	}
8040 	return 1;
8041 }
8042 
8043 
8044 ///
8045 class Window : Widget {
8046 	int mouseCaptureCount = 0;
8047 	Widget mouseCapturedBy;
8048 	void captureMouse(Widget byWhom) {
8049 		assert(mouseCapturedBy is null || byWhom is mouseCapturedBy);
8050 		mouseCaptureCount++;
8051 		mouseCapturedBy = byWhom;
8052 		win.grabInput(false, true, false);
8053 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
8054 	}
8055 	void releaseMouseCapture() {
8056 		mouseCaptureCount--;
8057 		mouseCapturedBy = null;
8058 		win.releaseInputGrab();
8059 	}
8060 
8061 
8062 	/++
8063 
8064 	+/
8065 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8066 		return .messageBox(this, title, message, style, icon);
8067 	}
8068 
8069 	/// ditto
8070 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8071 		return messageBox(null, message, style, icon);
8072 	}
8073 
8074 
8075 	/++
8076 		Sets the window icon which is often seen in title bars and taskbars.
8077 
8078 		History:
8079 			Added April 5, 2022 (dub v10.8)
8080 	+/
8081 	@property void icon(MemoryImage icon) {
8082 		if(win && icon)
8083 			win.icon = icon;
8084 	}
8085 
8086 	// 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
8087 	// this does NOT change the icon on the window! That's what the other overload is for
8088 	static @property .icon icon(GenericIcons i) {
8089 		return .icon(i);
8090 	}
8091 
8092 	///
8093 	@scriptable
8094 	@property bool focused() {
8095 		return win.focused;
8096 	}
8097 
8098 	static class Style : Widget.Style {
8099 		override WidgetBackground background() {
8100 			version(custom_widgets)
8101 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8102 			else version(win32_widgets)
8103 				return WidgetBackground(Color.transparent);
8104 			else static assert(0);
8105 		}
8106 	}
8107 	mixin OverrideStyle!Style;
8108 
8109 	/++
8110 		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.
8111 	+/
8112 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8113 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8114 	}
8115 
8116 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8117 		OperatingSystemFont font;
8118 		if(auto vt = WidgetPainter.visualTheme) {
8119 			font = vt.defaultFontCached(96); // FIXME
8120 		}
8121 
8122 		if(font is null) {
8123 			static int defaultHeightCache;
8124 			if(defaultHeightCache == 0) {
8125 				font = new OperatingSystemFont;
8126 				font.loadDefault;
8127 				defaultHeightCache = font.height();// * 5 / 4;
8128 			}
8129 			return defaultHeightCache;
8130 		}
8131 
8132 		return font.height();// * 5 / 4;
8133 	}
8134 
8135 	Widget focusedWidget;
8136 
8137 	private SimpleWindow win_;
8138 
8139 	@property {
8140 		/++
8141 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8142 
8143 			History:
8144 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8145 		+/
8146 		public SimpleWindow win() {
8147 			return win_;
8148 		}
8149 		///
8150 		protected void win(SimpleWindow w) {
8151 			win_ = w;
8152 		}
8153 	}
8154 
8155 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8156 	this(Widget p) {
8157 		tabStop = false;
8158 		super(p);
8159 	}
8160 
8161 	private void actualRedraw() {
8162 		if(recomputeChildLayoutRequired)
8163 			recomputeChildLayoutEntry();
8164 		if(!showing) return;
8165 
8166 		assert(parentWindow !is null);
8167 
8168 		auto w = drawableWindow;
8169 		if(w is null)
8170 			w = parentWindow.win;
8171 
8172 		if(w.closed())
8173 			return;
8174 
8175 		auto ugh = this.parent;
8176 		int lox, loy;
8177 		while(ugh) {
8178 			lox += ugh.x;
8179 			loy += ugh.y;
8180 			ugh = ugh.parent;
8181 		}
8182 		auto painter = w.draw(true);
8183 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8184 	}
8185 
8186 
8187 	private bool skipNextChar = false;
8188 
8189 	/++
8190 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8191 
8192 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8193 	+/
8194 	this(SimpleWindow win) {
8195 
8196 		static if(UsingSimpledisplayX11) {
8197 			win.discardAdditionalConnectionState = &discardXConnectionState;
8198 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8199 		}
8200 
8201 		tabStop = false;
8202 		super(null);
8203 		this.win = win;
8204 
8205 		win.addEventListener((Widget.RedrawEvent) {
8206 			if(win.eventQueued!RecomputeEvent) {
8207 				// writeln("skipping");
8208 				return; // let the recompute event do the actual redraw
8209 			}
8210 			this.actualRedraw();
8211 		});
8212 
8213 		win.addEventListener((Widget.RecomputeEvent) {
8214 			recomputeChildLayoutEntry();
8215 			if(win.eventQueued!RedrawEvent)
8216 				return; // let the queued one do it
8217 			else {
8218 				// writeln("drawing");
8219 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8220 			}
8221 		});
8222 
8223 		this.width = win.width;
8224 		this.height = win.height;
8225 		this.parentWindow = this;
8226 
8227 		win.closeQuery = () {
8228 			if(this.emit!ClosingEvent())
8229 				win.close();
8230 		};
8231 		win.onClosing = () {
8232 			this.emit!ClosedEvent();
8233 		};
8234 
8235 		win.windowResized = (int w, int h) {
8236 			this.width = w;
8237 			this.height = h;
8238 			queueRecomputeChildLayout();
8239 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8240 			//version(win32_widgets)
8241 				//InvalidateRect(hwnd, null, true);
8242 			redraw();
8243 		};
8244 
8245 		win.onFocusChange = (bool getting) {
8246 			if(this.focusedWidget) {
8247 				if(getting) {
8248 					this.focusedWidget.emit!FocusEvent();
8249 					this.focusedWidget.emit!FocusInEvent();
8250 				} else {
8251 					this.focusedWidget.emit!BlurEvent();
8252 					this.focusedWidget.emit!FocusOutEvent();
8253 				}
8254 			}
8255 
8256 			if(getting) {
8257 				this.emit!FocusEvent();
8258 				this.emit!FocusInEvent();
8259 			} else {
8260 				this.emit!BlurEvent();
8261 				this.emit!FocusOutEvent();
8262 			}
8263 		};
8264 
8265 		win.onDpiChanged = {
8266 			this.queueRecomputeChildLayout();
8267 			auto event = new DpiChangedEvent(this);
8268 			event.sendDirectly();
8269 
8270 			privateDpiChanged();
8271 		};
8272 
8273 		win.setEventHandlers(
8274 			(MouseEvent e) {
8275 				dispatchMouseEvent(e);
8276 			},
8277 			(KeyEvent e) {
8278 				//writefln("%x   %s", cast(uint) e.key, e.key);
8279 				dispatchKeyEvent(e);
8280 			},
8281 			(dchar e) {
8282 				if(e == 13) e = 10; // hack?
8283 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8284 				dispatchCharEvent(e);
8285 			},
8286 		);
8287 
8288 		addEventListener("char", (Widget, Event ev) {
8289 			if(skipNextChar) {
8290 				ev.preventDefault();
8291 				skipNextChar = false;
8292 			}
8293 		});
8294 
8295 		version(win32_widgets)
8296 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
8297 			if(hwnd !is this.win.impl.hwnd)
8298 				return 1; // we don't care... pass it on
8299 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
8300 			if(mustReturn)
8301 				return ret;
8302 			return 1; // pass it on
8303 		};
8304 
8305 		if(Window.newWindowCreated)
8306 			Window.newWindowCreated(this);
8307 	}
8308 
8309 	version(custom_widgets)
8310 	override void defaultEventHandler_click(ClickEvent event) {
8311 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
8312 			if(event.target && event.target.tabStop)
8313 				event.target.focus();
8314 		}
8315 	}
8316 
8317 	private static void delegate(Window) newWindowCreated;
8318 
8319 	version(win32_widgets)
8320 	override void paint(WidgetPainter painter) {
8321 		/*
8322 		RECT rect;
8323 		rect.right = this.width;
8324 		rect.bottom = this.height;
8325 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
8326 		*/
8327 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
8328 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
8329 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
8330 		// since the pen is null, to fill the whole space, we need the +1 on both.
8331 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
8332 		SelectObject(painter.impl.hdc, p);
8333 		SelectObject(painter.impl.hdc, b);
8334 	}
8335 	version(custom_widgets)
8336 	override void paint(WidgetPainter painter) {
8337 		auto cs = getComputedStyle();
8338 		painter.fillColor = cs.windowBackgroundColor;
8339 		painter.outlineColor = cs.windowBackgroundColor;
8340 		painter.drawRectangle(Point(0, 0), this.width, this.height);
8341 	}
8342 
8343 
8344 	override void defaultEventHandler_keydown(KeyDownEvent event) {
8345 		Widget _this = event.target;
8346 
8347 		if(event.key == Key.Tab) {
8348 			/* Window tab ordering is a recursive thingy with each group */
8349 
8350 			// FIXME inefficient
8351 			Widget[] helper(Widget p) {
8352 				if(p.hidden)
8353 					return null;
8354 				Widget[] childOrdering;
8355 
8356 				auto children = p.children.dup;
8357 
8358 				while(true) {
8359 					// UIs should be generally small, so gonna brute force it a little
8360 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
8361 
8362 					Widget smallestTab;
8363 					foreach(ref c; children) {
8364 						if(c is null) continue;
8365 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
8366 							smallestTab = c;
8367 							c = null;
8368 						}
8369 					}
8370 					if(smallestTab !is null) {
8371 						if(smallestTab.tabStop && !smallestTab.hidden)
8372 							childOrdering ~= smallestTab;
8373 						if(!smallestTab.hidden)
8374 							childOrdering ~= helper(smallestTab);
8375 					} else
8376 						break;
8377 
8378 				}
8379 
8380 				return childOrdering;
8381 			}
8382 
8383 			Widget[] tabOrdering = helper(this);
8384 
8385 			Widget recipient;
8386 
8387 			if(tabOrdering.length) {
8388 				bool seenThis = false;
8389 				Widget previous;
8390 				foreach(idx, child; tabOrdering) {
8391 					if(child is focusedWidget) {
8392 
8393 						if(event.shiftKey) {
8394 							if(idx == 0)
8395 								recipient = tabOrdering[$-1];
8396 							else
8397 								recipient = tabOrdering[idx - 1];
8398 							break;
8399 						}
8400 
8401 						seenThis = true;
8402 						if(idx + 1 == tabOrdering.length) {
8403 							// we're at the end, either move to the next group
8404 							// or start back over
8405 							recipient = tabOrdering[0];
8406 						}
8407 						continue;
8408 					}
8409 					if(seenThis) {
8410 						recipient = child;
8411 						break;
8412 					}
8413 					previous = child;
8414 				}
8415 			}
8416 
8417 			if(recipient !is null) {
8418 				//  writeln(typeid(recipient));
8419 				recipient.focus();
8420 
8421 				skipNextChar = true;
8422 			}
8423 		}
8424 
8425 		debug if(event.key == Key.F12) {
8426 			if(devTools) {
8427 				devTools.close();
8428 				devTools = null;
8429 			} else {
8430 				devTools = new DevToolWindow(this);
8431 				devTools.show();
8432 			}
8433 		}
8434 	}
8435 
8436 	debug DevToolWindow devTools;
8437 
8438 
8439 	/++
8440 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
8441 
8442 		History:
8443 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
8444 
8445 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
8446 	+/
8447 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
8448 		if(title is null) {
8449 			import core.runtime;
8450 			if(Runtime.args.length)
8451 				title = Runtime.args[0];
8452 		}
8453 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
8454 
8455 		static if(UsingSimpledisplayX11)
8456 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
8457 		///+
8458 		// for input proxy
8459 		auto display = XDisplayConnection.get;
8460 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
8461 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
8462 		XMapWindow(display, inputProxy);
8463 		// writefln("input proxy: 0x%0x", inputProxy);
8464 		this.inputProxy = new SimpleWindow(inputProxy);
8465 
8466 		XEvent lastEvent;
8467 		this.inputProxy.handleNativeEvent = (XEvent ev) {
8468 			lastEvent = ev;
8469 			return 1;
8470 		};
8471 		this.inputProxy.setEventHandlers(
8472 			(MouseEvent e) {
8473 				dispatchMouseEvent(e);
8474 			},
8475 			(KeyEvent e) {
8476 				//writefln("%x   %s", cast(uint) e.key, e.key);
8477 				if(dispatchKeyEvent(e)) {
8478 					// FIXME: i should trap error
8479 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
8480 						auto thing = nw.focusableWindow();
8481 						if(thing && thing.window) {
8482 							lastEvent.xkey.window = thing.window;
8483 							// writeln("sending event ", lastEvent.xkey);
8484 							trapXErrors( {
8485 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
8486 							});
8487 						}
8488 					}
8489 				}
8490 			},
8491 			(dchar e) {
8492 				if(e == 13) e = 10; // hack?
8493 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8494 				dispatchCharEvent(e);
8495 			},
8496 		);
8497 
8498 		this.inputProxy.populateXic();
8499 		// done
8500 		//+/
8501 		}
8502 
8503 
8504 
8505 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
8506 
8507 		this(win);
8508 	}
8509 
8510 	SimpleWindow inputProxy;
8511 
8512 	private SimpleWindow setRequestedInputFocus() {
8513 		return inputProxy;
8514 	}
8515 
8516 	/// ditto
8517 	this(string title, int width = 500, int height = 500) {
8518 		this(width, height, title);
8519 	}
8520 
8521 	///
8522 	@property string title() { return parentWindow.win.title; }
8523 	///
8524 	@property void title(string title) { parentWindow.win.title = title; }
8525 
8526 	///
8527 	@scriptable
8528 	void close() {
8529 		win.close();
8530 		// I synchronize here upon window closing to ensure all child windows
8531 		// get updated too before the event loop. This avoids some random X errors.
8532 		static if(UsingSimpledisplayX11) {
8533 			runInGuiThread( {
8534 				XSync(XDisplayConnection.get, false);
8535 			});
8536 		}
8537 	}
8538 
8539 	bool dispatchKeyEvent(KeyEvent ev) {
8540 		auto wid = focusedWidget;
8541 		if(wid is null)
8542 			wid = this;
8543 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
8544 		event.originalKeyEvent = ev;
8545 		event.key = ev.key;
8546 		event.state = ev.modifierState;
8547 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8548 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8549 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8550 		event.dispatch();
8551 
8552 		return !event.propagationStopped;
8553 	}
8554 
8555 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
8556 	bool dispatchCharEvent(dchar ch) {
8557 		if(focusedWidget) {
8558 			auto event = new CharEvent(focusedWidget, ch);
8559 			event.dispatch();
8560 			return !event.propagationStopped;
8561 		}
8562 		return true;
8563 	}
8564 
8565 	Widget mouseLastOver;
8566 	Widget mouseLastDownOn;
8567 	bool lastWasDoubleClick;
8568 	bool dispatchMouseEvent(MouseEvent ev) {
8569 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
8570 		auto ele = eleR.widget;
8571 
8572 		auto captureEle = ele;
8573 
8574 		if(mouseCapturedBy !is null) {
8575 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
8576 				captureEle = mouseCapturedBy;
8577 		}
8578 
8579 		// a hack to get it relative to the widget.
8580 		eleR.x = ev.x;
8581 		eleR.y = ev.y;
8582 		auto pain = captureEle;
8583 		while(pain) {
8584 			eleR.x -= pain.x;
8585 			eleR.y -= pain.y;
8586 			pain.addScrollPosition(eleR.x, eleR.y);
8587 			pain = pain.parent;
8588 		}
8589 
8590 		void populateMouseEventBase(MouseEventBase event) {
8591 			event.button = ev.button;
8592 			event.buttonLinear = ev.buttonLinear;
8593 			event.state = ev.modifierState;
8594 			event.clientX = eleR.x;
8595 			event.clientY = eleR.y;
8596 
8597 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
8598 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
8599 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
8600 		}
8601 
8602 		if(ev.type == MouseEventType.buttonPressed) {
8603 			{
8604 				auto event = new MouseDownEvent(captureEle);
8605 				populateMouseEventBase(event);
8606 				event.dispatch();
8607 			}
8608 
8609 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
8610 				auto event = new DoubleClickEvent(captureEle);
8611 				populateMouseEventBase(event);
8612 				event.dispatch();
8613 				lastWasDoubleClick = ev.doubleClick;
8614 			} else {
8615 				lastWasDoubleClick = false;
8616 			}
8617 
8618 			mouseLastDownOn = ele;
8619 		} else if(ev.type == MouseEventType.buttonReleased) {
8620 			{
8621 				auto event = new MouseUpEvent(captureEle);
8622 				populateMouseEventBase(event);
8623 				event.dispatch();
8624 			}
8625 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
8626 				auto event = new ClickEvent(captureEle);
8627 				populateMouseEventBase(event);
8628 				event.dispatch();
8629 			}
8630 		} else if(ev.type == MouseEventType.motion) {
8631 			// motion
8632 			{
8633 				auto event = new MouseMoveEvent(captureEle);
8634 				populateMouseEventBase(event); // fills in button which is meaningless but meh
8635 				event.dispatch();
8636 			}
8637 
8638 			if(mouseLastOver !is ele) {
8639 				if(ele !is null) {
8640 					if(!isAParentOf(ele, mouseLastOver)) {
8641 						ele.setDynamicState(DynamicState.hover, true);
8642 						auto event = new MouseEnterEvent(ele);
8643 						event.relatedTarget = mouseLastOver;
8644 						event.sendDirectly();
8645 
8646 						ele.useStyleProperties((scope Widget.Style s) {
8647 							ele.parentWindow.win.cursor = s.cursor;
8648 						});
8649 					}
8650 				}
8651 
8652 				if(mouseLastOver !is null) {
8653 					if(!isAParentOf(mouseLastOver, ele)) {
8654 						mouseLastOver.setDynamicState(DynamicState.hover, false);
8655 						auto event = new MouseLeaveEvent(mouseLastOver);
8656 						event.relatedTarget = ele;
8657 						event.sendDirectly();
8658 					}
8659 				}
8660 
8661 				if(ele !is null) {
8662 					auto event = new MouseOverEvent(ele);
8663 					event.relatedTarget = mouseLastOver;
8664 					event.dispatch();
8665 				}
8666 
8667 				if(mouseLastOver !is null) {
8668 					auto event = new MouseOutEvent(mouseLastOver);
8669 					event.relatedTarget = ele;
8670 					event.dispatch();
8671 				}
8672 
8673 				mouseLastOver = ele;
8674 			}
8675 		}
8676 
8677 		return true; // FIXME: the event default prevented?
8678 	}
8679 
8680 	/++
8681 		Shows the window and runs the application event loop.
8682 
8683 		Blocks until this window is closed.
8684 
8685 		Bugs:
8686 
8687 		$(PITFALL
8688 			You should always have one event loop live for your application.
8689 			If you make two windows in sequence, the second call to loop (or
8690 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
8691 			might fail:
8692 
8693 			---
8694 			// don't do this!
8695 			auto window = new Window();
8696 			window.loop();
8697 
8698 			// or new Window or new MainWindow, all the same
8699 			auto window2 = new SimpleWindow();
8700 			window2.eventLoop(0); // problematic! might crash
8701 			---
8702 
8703 			simpledisplay's current implementation assumes that final cleanup is
8704 			done when the event loop refcount reaches zero. So after the first
8705 			eventLoop returns, when there isn't already another one active, it assumes
8706 			the program will exit soon and cleans up.
8707 
8708 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
8709 			it eventually, but in the mean time, there's an easy solution:
8710 
8711 			---
8712 			// do this
8713 			EventLoop mainEventLoop = EventLoop.get; // just add this line
8714 
8715 			auto window = new Window();
8716 			window.loop();
8717 
8718 			// or any other type of Window etc.
8719 			auto window2 = new Window();
8720 			window2.loop(); // perfectly fine since mainEventLoop still alive
8721 			---
8722 
8723 			By adding a top-level reference to the event loop, it ensures the final cleanup
8724 			is not performed until it goes out of scope too, letting the individual window loops
8725 			work without trouble despite the bug.
8726 		)
8727 
8728 		History:
8729 			The [BlockingMode] parameter was added on December 8, 2021.
8730 			The default behavior is to block until the application quits
8731 			(so all windows have been closed), unless another minigui or
8732 			simpledisplay event loop is already running, in which case it
8733 			will block until this window closes specifically.
8734 	+/
8735 	@scriptable
8736 	void loop(BlockingMode bm = BlockingMode.automatic) {
8737 		if(win.closed)
8738 			return; // otherwise show will throw
8739 		show();
8740 		win.eventLoopWithBlockingMode(bm, 0);
8741 	}
8742 
8743 	private bool firstShow = true;
8744 
8745 	@scriptable
8746 	override void show() {
8747 		bool rd = false;
8748 		if(firstShow) {
8749 			firstShow = false;
8750 			queueRecomputeChildLayout();
8751 			auto f = getFirstFocusable(this); // FIXME: autofocus?
8752 			if(f)
8753 				f.focus();
8754 			redraw();
8755 		}
8756 		win.show();
8757 		super.show();
8758 	}
8759 	@scriptable
8760 	override void hide() {
8761 		win.hide();
8762 		super.hide();
8763 	}
8764 
8765 	static Widget getFirstFocusable(Widget start) {
8766 		if(start is null)
8767 			return null;
8768 
8769 		foreach(widget; &start.focusableWidgets) {
8770 			return widget;
8771 		}
8772 
8773 		return null;
8774 	}
8775 
8776 	static Widget getLastFocusable(Widget start) {
8777 		if(start is null)
8778 			return null;
8779 
8780 		Widget last;
8781 		foreach(widget; &start.focusableWidgets) {
8782 			last = widget;
8783 		}
8784 
8785 		return last;
8786 	}
8787 
8788 
8789 	mixin Emits!ClosingEvent;
8790 	mixin Emits!ClosedEvent;
8791 }
8792 
8793 /++
8794 	History:
8795 		Added January 12, 2022
8796 +/
8797 class DpiChangedEvent : Event {
8798 	enum EventString = "dpichanged";
8799 
8800 	this(Widget target) {
8801 		super(EventString, target);
8802 	}
8803 }
8804 
8805 debug private class DevToolWindow : Window {
8806 	Window p;
8807 
8808 	TextEdit parentList;
8809 	TextEdit logWindow;
8810 	TextLabel clickX, clickY;
8811 
8812 	this(Window p) {
8813 		this.p = p;
8814 		super(400, 300, "Developer Toolbox");
8815 
8816 		logWindow = new TextEdit(this);
8817 		parentList = new TextEdit(this);
8818 
8819 		auto hl = new HorizontalLayout(this);
8820 		clickX = new TextLabel("", TextAlignment.Right, hl);
8821 		clickY = new TextLabel("", TextAlignment.Right, hl);
8822 
8823 		parentListeners ~= p.addEventListener("*", (Event ev) {
8824 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
8825 		});
8826 
8827 		parentListeners ~= p.addEventListener((ClickEvent ev) {
8828 			auto s = ev.srcElement;
8829 
8830 			string list;
8831 
8832 			void addInfo(Widget s) {
8833 				list ~= s.toString();
8834 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
8835 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
8836 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
8837 				list ~= "\n\theight: " ~ toInternal!string(s.height);
8838 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
8839 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
8840 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
8841 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
8842 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
8843 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
8844 			}
8845 
8846 			addInfo(s);
8847 
8848 			s = s.parent;
8849 			while(s) {
8850 				list ~= "\n";
8851 				addInfo(s);
8852 				s = s.parent;
8853 			}
8854 			parentList.content = list;
8855 
8856 			clickX.label = toInternal!string(ev.clientX);
8857 			clickY.label = toInternal!string(ev.clientY);
8858 		});
8859 	}
8860 
8861 	EventListener[] parentListeners;
8862 
8863 	override void close() {
8864 		assert(p !is null);
8865 		foreach(p; parentListeners)
8866 			p.disconnect();
8867 		parentListeners = null;
8868 		p.devTools = null;
8869 		p = null;
8870 		super.close();
8871 	}
8872 
8873 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
8874 		if(ev.key == Key.F12) {
8875 			this.close();
8876 			if(p)
8877 				p.devTools = null;
8878 		} else {
8879 			super.defaultEventHandler_keydown(ev);
8880 		}
8881 	}
8882 
8883 	void log(T...)(T t) {
8884 		string str;
8885 		import std.conv;
8886 		foreach(i; t)
8887 			str ~= to!string(i);
8888 		str ~= "\n";
8889 		logWindow.addText(str);
8890 
8891 		//version(custom_widgets)
8892 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
8893 	}
8894 }
8895 
8896 /++
8897 	A dialog is a transient window that intends to get information from
8898 	the user before being dismissed.
8899 +/
8900 class Dialog : Window {
8901 	///
8902 	this(Window parent, int width, int height, string title = null) {
8903 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
8904 
8905 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
8906 	}
8907 
8908 	///
8909 	this(Window parent, string title, int width, int height) {
8910 		this(parent, width, height, title);
8911 	}
8912 
8913 	deprecated("Pass an explicit parent window, even if it is `null`")
8914 	this(int width, int height, string title = null) {
8915 		this(null, width, height, title);
8916 	}
8917 
8918 	///
8919 	void OK() {
8920 
8921 	}
8922 
8923 	///
8924 	void Cancel() {
8925 		this.close();
8926 	}
8927 }
8928 
8929 /++
8930 	A custom widget similar to the HTML5 <details> tag.
8931 +/
8932 version(none)
8933 class DetailsView : Widget {
8934 
8935 }
8936 
8937 // FIXME: maybe i should expose the other list views Windows offers too
8938 
8939 /++
8940 	A TableView is a widget made to display a table of data strings.
8941 
8942 
8943 	Future_Directions:
8944 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
8945 
8946 		I will add a selection changed event at some point, as well as item clicked events.
8947 	History:
8948 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
8949 	See_Also:
8950 		[ListWidget] which displays a list of strings without additional columns.
8951 +/
8952 class TableView : Widget {
8953 	/++
8954 
8955 	+/
8956 	this(Widget parent) {
8957 		super(parent);
8958 
8959 		version(win32_widgets) {
8960 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
8961 		} else version(custom_widgets) {
8962 			auto smw = new ScrollMessageWidget(this);
8963 			smw.addDefaultKeyboardListeners();
8964 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
8965 			tvwi = new TableViewWidgetInner(this, smw);
8966 		}
8967 	}
8968 
8969 	// FIXME: auto-size columns on double click of header thing like in Windows
8970 	// it need only make the currently displayed things fit well.
8971 
8972 
8973 	private ColumnInfo[] columns;
8974 	private int itemCount;
8975 
8976 	version(custom_widgets) private {
8977 		TableViewWidgetInner tvwi;
8978 	}
8979 
8980 	/// Passed to [setColumnInfo]
8981 	static struct ColumnInfo {
8982 		const(char)[] name; /// the name displayed in the header
8983 		/++
8984 			The default width, in pixels. As a special case, you can set this to -1
8985 			if you want the system to try to automatically size the width to fit visible
8986 			content. If it can't, it will try to pick a sensible default size.
8987 
8988 			Any other negative value is not allowed and may lead to unpredictable results.
8989 
8990 			History:
8991 				The -1 behavior was specified on December 3, 2021. It actually worked before
8992 				anyway on Win32 but now it is a formal feature with partial Linux support.
8993 
8994 			Bugs:
8995 				It doesn't actually attempt to calculate a best-fit width on Linux as of
8996 				December 3, 2021. I do plan to fix this in the future, but Windows is the
8997 				priority right now. At least it doesn't break things when you use it now.
8998 		+/
8999 		int width;
9000 
9001 		/++
9002 			Alignment of the text in the cell. Applies to the header as well as all data in this
9003 			column.
9004 
9005 			Bugs:
9006 				On Windows, the first column ignores this member and is always left aligned.
9007 				You can work around this by inserting a dummy first column with width = 0
9008 				then putting your actual data in the second column, which does respect the
9009 				alignment.
9010 
9011 				This is a quirk of the operating system's implementation going back a very
9012 				long time and is unlikely to ever be fixed.
9013 		+/
9014 		TextAlignment alignment;
9015 
9016 		/++
9017 			After all the pixel widths have been assigned, any left over
9018 			space is divided up among all columns and distributed to according
9019 			to the widthPercent field.
9020 
9021 
9022 			For example, if you have two fields, both with width 50 and one with
9023 			widthPercent of 25 and the other with widthPercent of 75, and the
9024 			container is 200 pixels wide, first both get their width of 50.
9025 			then the 100 remaining pixels are split up, so the one gets a total
9026 			of 75 pixels and the other gets a total of 125.
9027 
9028 			This is automatically applied as the window is resized.
9029 
9030 			If there is not enough space - that is, when a horizontal scrollbar
9031 			needs to appear - there are 0 pixels divided up, and thus everyone
9032 			gets 0. This can cause a column to shrink out of proportion when
9033 			passing the scroll threshold.
9034 
9035 			It is important to still set a fixed width (that is, to populate the
9036 			`width` field) even if you use the percents because that will be the
9037 			default minimum in the event of a scroll bar appearing.
9038 
9039 			The percents total in the column can never exceed 100 or be less than 0.
9040 			Doing this will trigger an assert error.
9041 
9042 			Implementation note:
9043 
9044 			Please note that percentages are only recalculated 1) upon original
9045 			construction and 2) upon resizing the control. If the user adjusts the
9046 			width of a column, the percentage items will not be updated.
9047 
9048 			On the other hand, if the user adjusts the width of a percentage column
9049 			then resizes the window, it is recalculated, meaning their hand adjustment
9050 			is discarded. This specific behavior may change in the future as it is
9051 			arguably a bug, but I'm not certain yet.
9052 
9053 			History:
9054 				Added November 10, 2021 (dub v10.4)
9055 		+/
9056 		int widthPercent;
9057 
9058 
9059 		private int calculatedWidth;
9060 	}
9061 	/++
9062 		Sets the number of columns along with information about the headers.
9063 
9064 		Please note: on Windows, the first column ignores your alignment preference
9065 		and is always left aligned.
9066 	+/
9067 	void setColumnInfo(ColumnInfo[] columns...) {
9068 
9069 		foreach(ref c; columns) {
9070 			c.name = c.name.idup;
9071 		}
9072 		this.columns = columns.dup;
9073 
9074 		updateCalculatedWidth(false);
9075 
9076 		version(custom_widgets) {
9077 			tvwi.header.updateHeaders();
9078 			tvwi.updateScrolls();
9079 		} else version(win32_widgets)
9080 		foreach(i, column; this.columns) {
9081 			LVCOLUMN lvColumn;
9082 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
9083 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
9084 
9085 			auto bfr = WCharzBuffer(column.name);
9086 			lvColumn.pszText = bfr.ptr;
9087 
9088 			if(column.alignment & TextAlignment.Center)
9089 				lvColumn.fmt = LVCFMT_CENTER;
9090 			else if(column.alignment & TextAlignment.Right)
9091 				lvColumn.fmt = LVCFMT_RIGHT;
9092 			else
9093 				lvColumn.fmt = LVCFMT_LEFT;
9094 
9095 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
9096 				throw new WindowsApiException("Insert Column Fail", GetLastError());
9097 		}
9098 	}
9099 
9100 	private int getActualSetSize(size_t i, bool askWindows) {
9101 		version(win32_widgets)
9102 			if(askWindows)
9103 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
9104 		auto w = columns[i].width;
9105 		if(w == -1)
9106 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9107 		return w;
9108 	}
9109 
9110 	private void updateCalculatedWidth(bool informWindows) {
9111 		int padding;
9112 		version(win32_widgets)
9113 			padding = 4;
9114 		int remaining = this.width;
9115 		foreach(i, column; columns)
9116 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9117 		remaining -= padding;
9118 		if(remaining < 0)
9119 			remaining = 0;
9120 
9121 		int percentTotal;
9122 		foreach(i, ref column; columns) {
9123 			percentTotal += column.widthPercent;
9124 
9125 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9126 
9127 			column.calculatedWidth = c;
9128 
9129 			version(win32_widgets)
9130 			if(informWindows)
9131 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9132 		}
9133 
9134 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9135 		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).");
9136 
9137 
9138 	}
9139 
9140 	override void registerMovement() {
9141 		super.registerMovement();
9142 
9143 		updateCalculatedWidth(true);
9144 	}
9145 
9146 	/++
9147 		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.
9148 	+/
9149 	void setItemCount(int count) {
9150 		this.itemCount = count;
9151 		version(custom_widgets) {
9152 			tvwi.updateScrolls();
9153 			redraw();
9154 		} else version(win32_widgets) {
9155 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9156 		}
9157 	}
9158 
9159 	/++
9160 		Clears all items;
9161 	+/
9162 	void clear() {
9163 		this.itemCount = 0;
9164 		this.columns = null;
9165 		version(custom_widgets) {
9166 			tvwi.header.updateHeaders();
9167 			tvwi.updateScrolls();
9168 			redraw();
9169 		} else version(win32_widgets) {
9170 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9171 		}
9172 	}
9173 
9174 	/+
9175 	version(win32_widgets)
9176 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9177 		auto itemId = dis.itemID;
9178 		auto hdc = dis.hDC;
9179 		auto rect = dis.rcItem;
9180 		switch(dis.itemAction) {
9181 			case ODA_DRAWENTIRE:
9182 
9183 				// FIXME: do other items
9184 				// FIXME: do the focus rectangle i guess
9185 				// FIXME: alignment
9186 				// FIXME: column width
9187 				// FIXME: padding left
9188 				// FIXME: check dpi scaling
9189 				// FIXME: don't owner draw unless it is necessary.
9190 
9191 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9192 				RECT itemRect;
9193 				itemRect.top = 1; // subitem idx, 1-based
9194 				itemRect.left = LVIR_BOUNDS;
9195 
9196 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9197 				itemRect.left += padding;
9198 
9199 				getData(itemId, 0, (in char[] data) {
9200 					auto wdata = WCharzBuffer(data);
9201 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9202 
9203 				});
9204 			goto case;
9205 			case ODA_FOCUS:
9206 				if(dis.itemState & ODS_FOCUS)
9207 					DrawFocusRect(hdc, &rect);
9208 			break;
9209 			case ODA_SELECT:
9210 				// itemState & ODS_SELECTED
9211 			break;
9212 			default:
9213 		}
9214 		return 1;
9215 	}
9216 	+/
9217 
9218 	version(win32_widgets) {
9219 		CellStyle last;
9220 		COLORREF defaultColor;
9221 		COLORREF defaultBackground;
9222 	}
9223 
9224 	version(win32_widgets)
9225 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9226 		switch(code) {
9227 			case NM_CUSTOMDRAW:
9228 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9229 				switch(s.nmcd.dwDrawStage) {
9230 					case CDDS_PREPAINT:
9231 						if(getCellStyle is null)
9232 							return 0;
9233 
9234 						mustReturn = true;
9235 						return CDRF_NOTIFYITEMDRAW;
9236 					case CDDS_ITEMPREPAINT:
9237 						mustReturn = true;
9238 						return CDRF_NOTIFYSUBITEMDRAW;
9239 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9240 						mustReturn = true;
9241 
9242 						if(getCellStyle is null) // this SHOULD never happen...
9243 							return 0;
9244 
9245 						if(s.iSubItem == 0) {
9246 							// Windows resets it per row so we'll use item 0 as a chance
9247 							// to capture these for later
9248 							defaultColor = s.clrText;
9249 							defaultBackground = s.clrTextBk;
9250 						}
9251 
9252 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9253 						// if no special style and no reset needed...
9254 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9255 							return 0; // allow default processing to continue
9256 
9257 						last = style;
9258 
9259 						// might still need to reset or use the preference.
9260 
9261 						if(style.flags & CellStyle.Flags.textColorSet)
9262 							s.clrText = style.textColor.asWindowsColorRef;
9263 						else
9264 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
9265 						if(style.flags & CellStyle.Flags.backgroundColorSet)
9266 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
9267 						else
9268 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
9269 
9270 						return CDRF_NEWFONT;
9271 					default:
9272 						return 0;
9273 
9274 				}
9275 			case NM_RETURN: // no need since i subclass keydown
9276 			break;
9277 			case LVN_COLUMNCLICK:
9278 				auto info = cast(LPNMLISTVIEW) hdr;
9279 				this.emit!HeaderClickedEvent(info.iSubItem);
9280 			break;
9281 			case NM_CLICK:
9282 			case NM_DBLCLK:
9283 			case NM_RCLICK:
9284 			case NM_RDBLCLK:
9285 				// the item/subitem is set here and that can be a useful notification
9286 				// even beyond the normal click notification
9287 			break;
9288 			case LVN_GETDISPINFO:
9289 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
9290 				if(info.item.mask & LVIF_TEXT) {
9291 					if(getData) {
9292 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
9293 							auto bfr = WCharzBuffer(dataReceived);
9294 							auto len = info.item.cchTextMax;
9295 							if(bfr.length < len)
9296 								len = cast(typeof(len)) bfr.length;
9297 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
9298 							info.item.pszText[len] = 0;
9299 						});
9300 					} else {
9301 						info.item.pszText[0] = 0;
9302 					}
9303 					//info.item.iItem
9304 					//if(info.item.iSubItem)
9305 				}
9306 			break;
9307 			default:
9308 		}
9309 		return 0;
9310 	}
9311 
9312 	override bool encapsulatedChildren() {
9313 		return true;
9314 	}
9315 
9316 	/++
9317 		Informs the control that content has changed.
9318 
9319 		History:
9320 			Added November 10, 2021 (dub v10.4)
9321 	+/
9322 	void update() {
9323 		version(custom_widgets)
9324 			redraw();
9325 		else {
9326 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
9327 			UpdateWindow(hwnd);
9328 		}
9329 
9330 
9331 	}
9332 
9333 	/++
9334 		Called by the system to request the text content of an individual cell. You
9335 		should pass the text into the provided `sink` delegate. This function will be
9336 		called for each visible cell as-needed when drawing.
9337 	+/
9338 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
9339 
9340 	/++
9341 		Available per-cell style customization options. Use one of the constructors
9342 		provided to set the values conveniently, or default construct it and set individual
9343 		values yourself. Just remember to set the `flags` so your values are actually used.
9344 		If the flag isn't set, the field is ignored and the system default is used instead.
9345 
9346 		This is returned by the [getCellStyle] delegate.
9347 
9348 		Examples:
9349 			---
9350 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
9351 			auto table = new TableView(window);
9352 			// snip: you would set up columns here
9353 
9354 			// this is how you provide data to the table view class
9355 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
9356 				import std.conv;
9357 				sink(to!string(my_data[row][column]));
9358 			};
9359 
9360 			// and this is how you customize the colors
9361 			table.getCellStyle = delegate(int row, int column) {
9362 				return (my_data[row][column] < 0) ?
9363 					TableView.CellStyle(Color.red); // make negative numbers red
9364 					: TableView.CellStyle.init; // leave the rest alone
9365 			};
9366 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
9367 			---
9368 
9369 		History:
9370 			Added November 27, 2021 (dub v10.4)
9371 	+/
9372 	struct CellStyle {
9373 		/// 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.
9374 		this(Color textColor) {
9375 			this.textColor = textColor;
9376 			this.flags |= Flags.textColorSet;
9377 		}
9378 		/// Sets a custom text and background color.
9379 		this(Color textColor, Color backgroundColor) {
9380 			this.textColor = textColor;
9381 			this.backgroundColor = backgroundColor;
9382 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
9383 		}
9384 
9385 		Color textColor;
9386 		Color backgroundColor;
9387 		int flags; /// bitmask of [Flags]
9388 		/// available options to combine into [flags]
9389 		enum Flags {
9390 			textColorSet = 1 << 0,
9391 			backgroundColorSet = 1 << 1,
9392 		}
9393 	}
9394 	/++
9395 		Companion delegate to [getData] that allows you to custom style each
9396 		cell of the table.
9397 
9398 		Returns:
9399 			A [CellStyle] structure that describes the desired style for the
9400 			given cell. `return CellStyle.init` if you want the default style.
9401 
9402 		History:
9403 			Added November 27, 2021 (dub v10.4)
9404 	+/
9405 	CellStyle delegate(int row, int column) getCellStyle;
9406 
9407 	// i want to be able to do things like draw little colored things to show red for negative numbers
9408 	// or background color indicators or even in-cell charts
9409 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
9410 
9411 	/++
9412 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
9413 	+/
9414 	mixin Emits!HeaderClickedEvent;
9415 }
9416 
9417 /++
9418 	This is emitted by the [TableView] when a user clicks on a column header.
9419 
9420 	Its member `columnIndex` has the zero-based index of the column that was clicked.
9421 
9422 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
9423 
9424 	History:
9425 		Added November 27, 2021 (dub v10.4)
9426 +/
9427 class HeaderClickedEvent : Event {
9428 	enum EventString = "HeaderClicked";
9429 	this(Widget target, int columnIndex) {
9430 		this.columnIndex = columnIndex;
9431 		super(EventString, target);
9432 	}
9433 
9434 	/// The index of the column
9435 	int columnIndex;
9436 
9437 	///
9438 	override @property int intValue() {
9439 		return columnIndex;
9440 	}
9441 }
9442 
9443 version(custom_widgets)
9444 private class TableViewWidgetInner : Widget {
9445 
9446 // wrap this thing in a ScrollMessageWidget
9447 
9448 	TableView tvw;
9449 	ScrollMessageWidget smw;
9450 	HeaderWidget header;
9451 
9452 	this(TableView tvw, ScrollMessageWidget smw) {
9453 		this.tvw = tvw;
9454 		this.smw = smw;
9455 		super(smw);
9456 
9457 		this.tabStop = true;
9458 
9459 		header = new HeaderWidget(this, smw.getHeader());
9460 
9461 		smw.addEventListener("scroll", () {
9462 			this.redraw();
9463 			header.redraw();
9464 		});
9465 
9466 
9467 		// I need headers outside the scroll area but rendered on the same line as the up arrow
9468 		// FIXME: add a fixed header to the SMW
9469 	}
9470 
9471 	enum padding = 3;
9472 
9473 	void updateScrolls() {
9474 		int w;
9475 		foreach(idx, column; tvw.columns) {
9476 			if(column.width == 0) continue;
9477 			w += tvw.getActualSetSize(idx, false);// + padding;
9478 		}
9479 		smw.setTotalArea(w, tvw.itemCount);
9480 		columnsWidth = w;
9481 	}
9482 
9483 	private int columnsWidth;
9484 
9485 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
9486 
9487 	override void registerMovement() {
9488 		super.registerMovement();
9489 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
9490 		smw.setViewableArea(this.width, this.height / lh);
9491 	}
9492 
9493 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
9494 		int x;
9495 		int y;
9496 
9497 		int row = smw.position.y;
9498 
9499 		foreach(lol; 0 .. this.height / lh) {
9500 			if(row >= tvw.itemCount)
9501 				break;
9502 			x = 0;
9503 			foreach(columnNumber, column; tvw.columns) {
9504 				auto x2 = x + column.calculatedWidth;
9505 				auto smwx = smw.position.x;
9506 
9507 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
9508 					auto startX = x;
9509 					auto endX = x + column.calculatedWidth;
9510 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
9511 						case TextAlignment.Left: startX += padding; break;
9512 						case TextAlignment.Center: startX += padding; endX -= padding; break;
9513 						case TextAlignment.Right: endX -= padding; break;
9514 						default: /* broken */ break;
9515 					}
9516 					if(column.width != 0) // no point drawing an invisible column
9517 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
9518 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh)));
9519 
9520 						void dotext(WidgetPainter painter) {
9521 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
9522 						}
9523 
9524 						if(tvw.getCellStyle !is null) {
9525 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
9526 
9527 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
9528 								auto tempPainter = painter;
9529 								tempPainter.fillColor = style.backgroundColor;
9530 								tempPainter.outlineColor = style.backgroundColor;
9531 
9532 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
9533 									Point(endX - smw.position.x, y + lh));
9534 							}
9535 							auto tempPainter = painter;
9536 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
9537 								tempPainter.outlineColor = style.textColor;
9538 
9539 							dotext(tempPainter);
9540 						} else {
9541 							dotext(painter);
9542 						}
9543 					});
9544 				}
9545 
9546 				x += column.calculatedWidth;
9547 			}
9548 			row++;
9549 			y += lh;
9550 		}
9551 		return bounds;
9552 	}
9553 
9554 	static class Style : Widget.Style {
9555 		override WidgetBackground background() {
9556 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
9557 		}
9558 	}
9559 	mixin OverrideStyle!Style;
9560 
9561 	private static class HeaderWidget : Widget {
9562 		/+
9563 			maybe i should do a splitter thing on top of the other widgets
9564 			so the splitter itself isn't really drawn but still replies to mouse events?
9565 		+/
9566 		this(TableViewWidgetInner tvw, Widget parent) {
9567 			super(parent);
9568 			this.tvw = tvw;
9569 
9570 			this.remainder = new Button("", this);
9571 
9572 			this.addEventListener((scope ClickEvent ev) {
9573 				int header = -1;
9574 				foreach(idx, child; this.children[1 .. $]) {
9575 					if(child is ev.target) {
9576 						header = cast(int) idx;
9577 						break;
9578 					}
9579 				}
9580 
9581 				if(header != -1) {
9582 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
9583 					hce.dispatch();
9584 				}
9585 
9586 			});
9587 		}
9588 
9589 		void updateHeaders() {
9590 			foreach(child; children[1 .. $])
9591 				child.removeWidget();
9592 
9593 			foreach(column; tvw.tvw.columns) {
9594 				// the cast is ok because I dup it above, just the type is never changed.
9595 				// all this is private so it should never get messed up.
9596 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
9597 			}
9598 		}
9599 
9600 		Button remainder;
9601 		TableViewWidgetInner tvw;
9602 
9603 		override void recomputeChildLayout() {
9604 			registerMovement();
9605 			int pos;
9606 			foreach(idx, child; children[1 .. $]) {
9607 				if(idx >= tvw.tvw.columns.length)
9608 					continue;
9609 				child.x = pos;
9610 				child.y = 0;
9611 				child.width = tvw.tvw.columns[idx].calculatedWidth;
9612 				child.height = scaleWithDpi(16);// this.height;
9613 				pos += child.width;
9614 
9615 				child.recomputeChildLayout();
9616 			}
9617 
9618 			if(remainder is null)
9619 				return;
9620 
9621 			remainder.x = pos;
9622 			remainder.y = 0;
9623 			if(pos < this.width)
9624 				remainder.width = this.width - pos;// + 4;
9625 			else
9626 				remainder.width = 0;
9627 			remainder.height = scaleWithDpi(16);
9628 
9629 			remainder.recomputeChildLayout();
9630 		}
9631 
9632 		// for the scrollable children mixin
9633 		Point scrollOrigin() {
9634 			return Point(tvw.smw.position.x, 0);
9635 		}
9636 		void paintFrameAndBackground(WidgetPainter painter) { }
9637 
9638 		mixin ScrollableChildren;
9639 	}
9640 }
9641 
9642 /+
9643 
9644 // given struct / array / number / string / etc, make it viewable and editable
9645 class DataViewerWidget : Widget {
9646 
9647 }
9648 +/
9649 
9650 /++
9651 	A line edit box with an associated label.
9652 
9653 	History:
9654 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
9655 
9656 		```
9657 		Old: ________
9658 
9659 		New:
9660 		____________
9661 		```
9662 
9663 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
9664 
9665 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
9666 		horizontal label but left aligned. You may also consider a [GridLayout].
9667 +/
9668 alias LabeledLineEdit = Labeled!LineEdit;
9669 
9670 private int widthThatWouldFitChildLabels(Widget w) {
9671 	if(w is null)
9672 		return 0;
9673 
9674 	int max;
9675 
9676 	if(auto label = cast(TextLabel) w) {
9677 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
9678 	} else {
9679 		foreach(child; w.children) {
9680 			max = mymax(max, widthThatWouldFitChildLabels(child));
9681 		}
9682 	}
9683 
9684 	return max;
9685 }
9686 
9687 /++
9688 	History:
9689 		Added May 19, 2021
9690 +/
9691 class Labeled(T) : Widget {
9692 	///
9693 	this(string label, Widget parent) {
9694 		super(parent);
9695 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
9696 	}
9697 
9698 	/++
9699 		History:
9700 			The alignment parameter was added May 17, 2021
9701 	+/
9702 	this(string label, TextAlignment alignment, Widget parent) {
9703 		super(parent);
9704 		initialize!HorizontalLayout(label, alignment, parent);
9705 	}
9706 
9707 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
9708 		tabStop = false;
9709 		horizontal = is(L == HorizontalLayout);
9710 		auto hl = new L(this);
9711 		if(horizontal) {
9712 			static class SpecialTextLabel : TextLabel {
9713 				Widget outerParent;
9714 
9715 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
9716 					this.outerParent = outerParent;
9717 					super(label, alignment, parent);
9718 				}
9719 
9720 				override int flexBasisWidth() {
9721 					return widthThatWouldFitChildLabels(outerParent);
9722 				}
9723 				/+
9724 				override int widthShrinkiness() { return 0; }
9725 				override int widthStretchiness() { return 1; }
9726 				+/
9727 
9728 				override int paddingRight() { return 6; }
9729 				override int paddingLeft() { return 9; }
9730 
9731 				override int paddingTop() { return 3; }
9732 			}
9733 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
9734 		} else
9735 			this.label = new TextLabel(label, alignment, hl);
9736 		this.lineEdit = new T(hl);
9737 
9738 		this.label.labelFor = this.lineEdit;
9739 	}
9740 
9741 	private bool horizontal;
9742 
9743 	TextLabel label; ///
9744 	T lineEdit; ///
9745 
9746 	override int flexBasisWidth() { return 250; }
9747 	override int widthShrinkiness() { return 1; }
9748 
9749 	override int minHeight() {
9750 		return this.children[0].minHeight;
9751 	}
9752 	override int maxHeight() { return minHeight(); }
9753 	override int marginTop() { return 4; }
9754 	override int marginBottom() { return 4; }
9755 
9756 	// FIXME: i should prolly call it value as well as content tbh
9757 
9758 	///
9759 	@property string content() {
9760 		return lineEdit.content;
9761 	}
9762 	///
9763 	@property void content(string c) {
9764 		return lineEdit.content(c);
9765 	}
9766 
9767 	///
9768 	void selectAll() {
9769 		lineEdit.selectAll();
9770 	}
9771 
9772 	override void focus() {
9773 		lineEdit.focus();
9774 	}
9775 }
9776 
9777 /++
9778 	A labeled password edit.
9779 
9780 	History:
9781 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
9782 
9783 		The default parameters for the constructors were also removed on May 19, 2021
9784 +/
9785 alias LabeledPasswordEdit = Labeled!PasswordEdit;
9786 
9787 private string toMenuLabel(string s) {
9788 	string n;
9789 	n.reserve(s.length);
9790 	foreach(c; s)
9791 		if(c == '_')
9792 			n ~= ' ';
9793 		else
9794 			n ~= c;
9795 	return n;
9796 }
9797 
9798 private void autoExceptionHandler(Exception e) {
9799 	messageBox(e.msg);
9800 }
9801 
9802 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
9803 	static if(is(T : void delegate())) {
9804 		return () {
9805 			try
9806 				t();
9807 			catch(Exception e)
9808 				autoExceptionHandler(e);
9809 		};
9810 	} else static if(is(typeof(fn) Params == __parameters)) {
9811 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
9812 			return () {
9813 				void onOK(string s) {
9814 					member = s;
9815 					try
9816 						t(Params[0](s));
9817 					catch(Exception e)
9818 						autoExceptionHandler(e);
9819 				}
9820 
9821 				if(
9822 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
9823 					|| type == FileDialogType.Save)
9824 				{
9825 					getSaveFileName(window, &onOK, member, filters, null);
9826 				} else
9827 					getOpenFileName(window, &onOK, member, filters, null);
9828 			};
9829 		} else {
9830 			struct S {
9831 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
9832 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
9833 				} else mixin(q{
9834 				static foreach(idx, ignore; Params) {
9835 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
9836 				}
9837 				});
9838 			}
9839 			return () {
9840 				dialog(window, (S s) {
9841 					try {
9842 						static if(is(typeof(t) Ret == return)) {
9843 							static if(is(Ret == void)) {
9844 								t(s.tupleof);
9845 							} else {
9846 								auto ret = t(s.tupleof);
9847 								import std.conv;
9848 								messageBox(to!string(ret), "Returned Value");
9849 							}
9850 						}
9851 					} catch(Exception e)
9852 						autoExceptionHandler(e);
9853 				}, null, __traits(identifier, fn));
9854 			};
9855 		}
9856 	}
9857 }
9858 
9859 private template hasAnyRelevantAnnotations(a...) {
9860 	bool helper() {
9861 		bool any;
9862 		foreach(attr; a) {
9863 			static if(is(typeof(attr) == .menu))
9864 				any = true;
9865 			else static if(is(typeof(attr) == .toolbar))
9866 				any = true;
9867 			else static if(is(attr == .separator))
9868 				any = true;
9869 			else static if(is(typeof(attr) == .accelerator))
9870 				any = true;
9871 			else static if(is(typeof(attr) == .hotkey))
9872 				any = true;
9873 			else static if(is(typeof(attr) == .icon))
9874 				any = true;
9875 			else static if(is(typeof(attr) == .label))
9876 				any = true;
9877 			else static if(is(typeof(attr) == .tip))
9878 				any = true;
9879 		}
9880 		return any;
9881 	}
9882 
9883 	enum bool hasAnyRelevantAnnotations = helper();
9884 }
9885 
9886 /++
9887 	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.
9888 +/
9889 class MainWindow : Window {
9890 	///
9891 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
9892 		super(initialWidth, initialHeight, title);
9893 
9894 		_clientArea = new ClientAreaWidget();
9895 		_clientArea.x = 0;
9896 		_clientArea.y = 0;
9897 		_clientArea.width = this.width;
9898 		_clientArea.height = this.height;
9899 		_clientArea.tabStop = false;
9900 
9901 		super.addChild(_clientArea);
9902 
9903 		statusBar = new StatusBar(this);
9904 	}
9905 
9906 	/++
9907 		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).
9908 
9909 	---
9910         struct Commands {
9911                 @menu("File") {
9912 			@toolbar("") // adds it to a generic toolbar
9913                         void New() {}
9914                         void Open() {}
9915                         void Save() {}
9916                         @separator
9917                         void Exit() @accelerator("Alt+F4") @hotkey('x') {
9918                                 window.close();
9919                         }
9920                 }
9921 
9922                 @menu("Edit") {
9923 			@icon(GenericIcons.Undo)
9924                         void Undo() {
9925                                 undo();
9926                         }
9927                         @separator
9928                         void Cut() {}
9929                         void Copy() {}
9930                         void Paste() {}
9931                 }
9932 
9933                 @menu("Help") {
9934                         void About() {}
9935                 }
9936         }
9937 
9938         Commands commands;
9939 
9940         window.setMenuAndToolbarFromAnnotatedCode(commands);
9941 	---
9942 
9943 	Note that you can call this function multiple times and it will add the items in order to the given items.
9944 
9945 	+/
9946 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
9947 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9948 	}
9949 	/// ditto
9950 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
9951 		setMenuAndToolbarFromAnnotatedCode_internal(t);
9952 	}
9953 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
9954 		Action[] toolbarActions;
9955 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
9956 		Menu[string] mcs;
9957 
9958 		foreach(menu; menuBar.subMenus) {
9959 			mcs[menu.label] = menu;
9960 		}
9961 
9962 		foreach(memberName; __traits(derivedMembers, T)) {
9963 			static if(memberName != "this")
9964 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
9965 				.menu menu;
9966 				.toolbar toolbar;
9967 				bool separator;
9968 				.accelerator accelerator;
9969 				.hotkey hotkey;
9970 				.icon icon;
9971 				string label;
9972 				string tip;
9973 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
9974 					static if(is(typeof(attr) == .menu))
9975 						menu = attr;
9976 					else static if(is(typeof(attr) == .toolbar))
9977 						toolbar = attr;
9978 					else static if(is(attr == .separator))
9979 						separator = true;
9980 					else static if(is(typeof(attr) == .accelerator))
9981 						accelerator = attr;
9982 					else static if(is(typeof(attr) == .hotkey))
9983 						hotkey = attr;
9984 					else static if(is(typeof(attr) == .icon))
9985 						icon = attr;
9986 					else static if(is(typeof(attr) == .label))
9987 						label = attr.label;
9988 					else static if(is(typeof(attr) == .tip))
9989 						tip = attr.tip;
9990 				}
9991 
9992 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
9993 					ushort correctIcon = icon.id; // FIXME
9994 					if(label.length == 0)
9995 						label = memberName.toMenuLabel;
9996 
9997 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
9998 
9999 					auto action = new Action(label, correctIcon, handler);
10000 
10001 					if(accelerator.keyString.length) {
10002 						auto ke = KeyEvent.parse(accelerator.keyString);
10003 						action.accelerator = ke;
10004 						accelerators[ke.toStr] = handler;
10005 					}
10006 
10007 					if(toolbar !is .toolbar.init)
10008 						toolbarActions ~= action;
10009 					if(menu !is .menu.init) {
10010 						Menu mc;
10011 						if(menu.name in mcs) {
10012 							mc = mcs[menu.name];
10013 						} else {
10014 							mc = new Menu(menu.name, this);
10015 							menuBar.addItem(mc);
10016 							mcs[menu.name] = mc;
10017 						}
10018 
10019 						if(separator)
10020 							mc.addSeparator();
10021 						mc.addItem(new MenuItem(action));
10022 					}
10023 				}
10024 			}
10025 		}
10026 
10027 		this.menuBar = menuBar;
10028 
10029 		if(toolbarActions.length) {
10030 			auto tb = new ToolBar(toolbarActions, this);
10031 		}
10032 	}
10033 
10034 	void delegate()[string] accelerators;
10035 
10036 	override void defaultEventHandler_keydown(KeyDownEvent event) {
10037 		auto str = event.originalKeyEvent.toStr;
10038 		if(auto acl = str in accelerators)
10039 			(*acl)();
10040 		super.defaultEventHandler_keydown(event);
10041 	}
10042 
10043 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
10044 		super.defaultEventHandler_mouseover(event);
10045 		if(this.statusBar !is null && event.target.statusTip.length)
10046 			this.statusBar.parts[0].content = event.target.statusTip;
10047 		else if(this.statusBar !is null && this.statusTip.length)
10048 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
10049 	}
10050 
10051 	override void addChild(Widget c, int position = int.max) {
10052 		if(auto tb = cast(ToolBar) c)
10053 			version(win32_widgets)
10054 				super.addChild(c, 0);
10055 			else version(custom_widgets)
10056 				super.addChild(c, menuBar ? 1 : 0);
10057 			else static assert(0);
10058 		else
10059 			clientArea.addChild(c, position);
10060 	}
10061 
10062 	ToolBar _toolBar;
10063 	///
10064 	ToolBar toolBar() { return _toolBar; }
10065 	///
10066 	ToolBar toolBar(ToolBar t) {
10067 		_toolBar = t;
10068 		foreach(child; this.children)
10069 			if(child is t)
10070 				return t;
10071 		version(win32_widgets)
10072 			super.addChild(t, 0);
10073 		else version(custom_widgets)
10074 			super.addChild(t, menuBar ? 1 : 0);
10075 		else static assert(0);
10076 		return t;
10077 	}
10078 
10079 	MenuBar _menu;
10080 	///
10081 	MenuBar menuBar() { return _menu; }
10082 	///
10083 	MenuBar menuBar(MenuBar m) {
10084 		if(m is _menu) {
10085 			version(custom_widgets)
10086 				queueRecomputeChildLayout();
10087 			return m;
10088 		}
10089 
10090 		if(_menu !is null) {
10091 			// make sure it is sanely removed
10092 			// FIXME
10093 		}
10094 
10095 		_menu = m;
10096 
10097 		version(win32_widgets) {
10098 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
10099 		} else version(custom_widgets) {
10100 			super.addChild(m, 0);
10101 
10102 		//	clientArea.y = menu.height;
10103 		//	clientArea.height = this.height - menu.height;
10104 
10105 			queueRecomputeChildLayout();
10106 		} else static assert(false);
10107 
10108 		return _menu;
10109 	}
10110 	private Widget _clientArea;
10111 	///
10112 	@property Widget clientArea() { return _clientArea; }
10113 	protected @property void clientArea(Widget wid) {
10114 		_clientArea = wid;
10115 	}
10116 
10117 	private StatusBar _statusBar;
10118 	/++
10119 		Returns the window's [StatusBar]. Be warned it may be `null`.
10120 	+/
10121 	@property StatusBar statusBar() { return _statusBar; }
10122 	/// ditto
10123 	@property void statusBar(StatusBar bar) {
10124 		if(_statusBar !is null)
10125 			_statusBar.removeWidget();
10126 		_statusBar = bar;
10127 		if(bar !is null)
10128 			super.addChild(_statusBar);
10129 	}
10130 }
10131 
10132 /+
10133 	This is really an implementation detail of [MainWindow]
10134 +/
10135 private class ClientAreaWidget : Widget {
10136 	this() {
10137 		this.tabStop = false;
10138 		super(null);
10139 		//sa = new ScrollableWidget(this);
10140 	}
10141 	/*
10142 	ScrollableWidget sa;
10143 	override void addChild(Widget w, int position) {
10144 		if(sa is null)
10145 			super.addChild(w, position);
10146 		else {
10147 			sa.addChild(w, position);
10148 			sa.setContentSize(this.minWidth + 1, this.minHeight);
10149 			writeln(sa.contentWidth, "x", sa.contentHeight);
10150 		}
10151 	}
10152 	*/
10153 }
10154 
10155 /**
10156 	Toolbars are lists of buttons (typically icons) that appear under the menu.
10157 	Each button ought to correspond to a menu item, represented by [Action] objects.
10158 */
10159 class ToolBar : Widget {
10160 	version(win32_widgets) {
10161 		private int idealHeight;
10162 		override int minHeight() { return idealHeight; }
10163 		override int maxHeight() { return idealHeight; }
10164 	} else version(custom_widgets) {
10165 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
10166 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
10167 	} else static assert(false);
10168 	override int heightStretchiness() { return 0; }
10169 
10170 	version(win32_widgets) {
10171 		HIMAGELIST imageListSmall;
10172 		HIMAGELIST imageListLarge;
10173 	}
10174 
10175 	this(Widget parent) {
10176 		this(null, parent);
10177 	}
10178 
10179 	version(win32_widgets)
10180 	void changeIconSize(bool useLarge) {
10181 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
10182 
10183 		/+
10184 		SIZE size;
10185 		import core.sys.windows.commctrl;
10186 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
10187 		idealHeight = size.cy + 4; // the plus 4 is a hack
10188 		+/
10189 
10190 		idealHeight = useLarge ? 34 : 26;
10191 
10192 		if(parent) {
10193 			parent.queueRecomputeChildLayout();
10194 			parent.redraw();
10195 		}
10196 
10197 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
10198 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
10199 	}
10200 
10201 	///
10202 	this(Action[] actions, Widget parent) {
10203 		super(parent);
10204 
10205 		tabStop = false;
10206 
10207 		version(win32_widgets) {
10208 			// so i like how the flat thing looks on windows, but not on wine
10209 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
10210 			// leave it commented
10211 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
10212 
10213 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
10214 
10215 			imageListSmall = ImageList_Create(
10216 				// width, height
10217 				16, 16,
10218 				ILC_COLOR16 | ILC_MASK,
10219 				16 /*numberOfButtons*/, 0);
10220 
10221 			imageListLarge = ImageList_Create(
10222 				// width, height
10223 				24, 24,
10224 				ILC_COLOR16 | ILC_MASK,
10225 				16 /*numberOfButtons*/, 0);
10226 
10227 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
10228 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
10229 
10230 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
10231 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
10232 
10233 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
10234 
10235 			TBBUTTON[] buttons;
10236 
10237 			// FIXME: I_IMAGENONE is if here is no icon
10238 			foreach(action; actions)
10239 				buttons ~= TBBUTTON(
10240 					MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
10241 					action.id,
10242 					TBSTATE_ENABLED, // state
10243 					0, // style
10244 					0, // reserved array, just zero it out
10245 					0, // dwData
10246 					cast(size_t) toWstringzInternal(action.label) // INT_PTR
10247 				);
10248 
10249 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
10250 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
10251 
10252 			/*
10253 			RECT rect;
10254 			GetWindowRect(hwnd, &rect);
10255 			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
10256 			*/
10257 
10258 			dpiChanged(); // to load the things calling changeIconSize the first time
10259 
10260 			assert(idealHeight);
10261 		} else version(custom_widgets) {
10262 			foreach(action; actions)
10263 				new ToolButton(action, this);
10264 		} else static assert(false);
10265 	}
10266 
10267 	override void recomputeChildLayout() {
10268 		.recomputeChildLayout!"width"(this);
10269 	}
10270 
10271 
10272 	version(win32_widgets)
10273 	override protected void dpiChanged() {
10274 		auto sz = scaleWithDpi(16);
10275 		if(sz >= 20)
10276 			changeIconSize(true);
10277 		else
10278 			changeIconSize(false);
10279 	}
10280 }
10281 
10282 enum toolbarIconSize = 24;
10283 
10284 /// 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.
10285 class ToolButton : Button {
10286 	///
10287 	this(string label, Widget parent) {
10288 		super(label, parent);
10289 		tabStop = false;
10290 	}
10291 	///
10292 	this(Action action, Widget parent) {
10293 		super(action.label, parent);
10294 		tabStop = false;
10295 		this.action = action;
10296 	}
10297 
10298 	version(custom_widgets)
10299 	override void defaultEventHandler_click(ClickEvent event) {
10300 		foreach(handler; action.triggered)
10301 			handler();
10302 	}
10303 
10304 	Action action;
10305 
10306 	override int maxWidth() { return toolbarIconSize; }
10307 	override int minWidth() { return toolbarIconSize; }
10308 	override int maxHeight() { return toolbarIconSize; }
10309 	override int minHeight() { return toolbarIconSize; }
10310 
10311 	version(custom_widgets)
10312 	override void paint(WidgetPainter painter) {
10313 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
10314 		painter.outlineColor = Color.black;
10315 
10316 		// I want to get from 16 to 24. that's * 3 / 2
10317 		static assert(toolbarIconSize >= 16);
10318 		enum multiplier = toolbarIconSize / 8;
10319 		enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0);
10320 		switch(action.iconId) {
10321 			case GenericIcons.New:
10322 				painter.fillColor = Color.white;
10323 				painter.drawPolygon(
10324 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor,
10325 					Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor,
10326 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor
10327 				);
10328 			break;
10329 			case GenericIcons.Save:
10330 				painter.fillColor = Color.white;
10331 				painter.outlineColor = Color.black;
10332 				painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10333 
10334 				// the label
10335 				painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor);
10336 
10337 				// the slider
10338 				painter.fillColor = Color.black;
10339 				painter.outlineColor = Color.black;
10340 				painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor);
10341 
10342 				painter.fillColor = Color.white;
10343 				painter.outlineColor = Color.white;
10344 				// the disc window
10345 				painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor);
10346 			break;
10347 			case GenericIcons.Open:
10348 				painter.fillColor = Color.white;
10349 				painter.drawPolygon(
10350 					Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor,
10351 					Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor);
10352 				painter.drawPolygon(
10353 					Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor,
10354 					Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor,
10355 					Point(2, 6) * multiplier / divisor);
10356 				//painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor);
10357 			break;
10358 			case GenericIcons.Copy:
10359 				painter.fillColor = Color.white;
10360 				painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor);
10361 				painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor);
10362 			break;
10363 			case GenericIcons.Cut:
10364 				painter.fillColor = Color.transparent;
10365 				painter.outlineColor = getComputedStyle.foregroundColor();
10366 				painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor);
10367 				painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor);
10368 				painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor);
10369 				painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor);
10370 			break;
10371 			case GenericIcons.Paste:
10372 				painter.fillColor = Color.white;
10373 				painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor);
10374 				painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
10375 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor);
10376 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor);
10377 				painter.fillColor = Color.black;
10378 				painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor);
10379 			break;
10380 			case GenericIcons.Help:
10381 				painter.outlineColor = getComputedStyle.foregroundColor();
10382 				painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10383 			break;
10384 			case GenericIcons.Undo:
10385 				painter.fillColor = Color.transparent;
10386 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10387 				painter.outlineColor = Color.black;
10388 				painter.fillColor = Color.black;
10389 				painter.drawPolygon(
10390 					Point(4, 4) * multiplier / divisor,
10391 					Point(8, 2) * multiplier / divisor,
10392 					Point(8, 6) * multiplier / divisor,
10393 					Point(4, 4) * multiplier / divisor,
10394 				);
10395 			break;
10396 			case GenericIcons.Redo:
10397 				painter.fillColor = Color.transparent;
10398 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
10399 				painter.outlineColor = Color.black;
10400 				painter.fillColor = Color.black;
10401 				painter.drawPolygon(
10402 					Point(10, 4) * multiplier / divisor,
10403 					Point(6, 2) * multiplier / divisor,
10404 					Point(6, 6) * multiplier / divisor,
10405 					Point(10, 4) * multiplier / divisor,
10406 				);
10407 			break;
10408 			default:
10409 				painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
10410 		}
10411 		return bounds;
10412 		});
10413 	}
10414 
10415 }
10416 
10417 
10418 /++
10419 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
10420 +/
10421 class MenuBar : Widget {
10422 	MenuItem[] items;
10423 	Menu[] subMenus;
10424 
10425 	version(win32_widgets) {
10426 		HMENU handle;
10427 		///
10428 		this(Widget parent = null) {
10429 			super(parent);
10430 
10431 			handle = CreateMenu();
10432 			tabStop = false;
10433 		}
10434 	} else version(custom_widgets) {
10435 		///
10436 		this(Widget parent = null) {
10437 			tabStop = false; // these are selected some other way
10438 			super(parent);
10439 		}
10440 
10441 		mixin Padding!q{2};
10442 	} else static assert(false);
10443 
10444 	version(custom_widgets)
10445 	override void paint(WidgetPainter painter) {
10446 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
10447 	}
10448 
10449 	///
10450 	MenuItem addItem(MenuItem item) {
10451 		this.addChild(item);
10452 		items ~= item;
10453 		version(win32_widgets) {
10454 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
10455 		}
10456 		return item;
10457 	}
10458 
10459 
10460 	///
10461 	Menu addItem(Menu item) {
10462 
10463 		subMenus ~= item;
10464 
10465 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
10466 
10467 		addChild(mbItem);
10468 		items ~= mbItem;
10469 
10470 		version(win32_widgets) {
10471 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
10472 		} else version(custom_widgets) {
10473 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
10474 				item.popup(mbItem);
10475 			};
10476 		} else static assert(false);
10477 
10478 		return item;
10479 	}
10480 
10481 	override void recomputeChildLayout() {
10482 		.recomputeChildLayout!"width"(this);
10483 	}
10484 
10485 	override int maxHeight() { return defaultLineHeight + 4; }
10486 	override int minHeight() { return defaultLineHeight + 4; }
10487 }
10488 
10489 
10490 /**
10491 	Status bars appear at the bottom of a MainWindow.
10492 	They are made out of Parts, with a width and content.
10493 
10494 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
10495 
10496 
10497 	sb.parts[0].content = "Status bar text!";
10498 */
10499 class StatusBar : Widget {
10500 	private Part[] partsArray;
10501 	///
10502 	struct Parts {
10503 		@disable this();
10504 		this(StatusBar owner) { this.owner = owner; }
10505 		//@disable this(this);
10506 		///
10507 		@property int length() { return cast(int) owner.partsArray.length; }
10508 		private StatusBar owner;
10509 		private this(StatusBar owner, Part[] parts) {
10510 			this.owner.partsArray = parts;
10511 			this.owner = owner;
10512 		}
10513 		///
10514 		Part opIndex(int p) {
10515 			if(owner.partsArray.length == 0)
10516 				this ~= new StatusBar.Part(0);
10517 			return owner.partsArray[p];
10518 		}
10519 
10520 		///
10521 		Part opOpAssign(string op : "~" )(Part p) {
10522 			assert(owner.partsArray.length < 255);
10523 			p.owner = this.owner;
10524 			p.idx = cast(int) owner.partsArray.length;
10525 			owner.partsArray ~= p;
10526 
10527 			owner.queueRecomputeChildLayout();
10528 
10529 			version(win32_widgets) {
10530 				int[256] pos;
10531 				int cpos;
10532 				foreach(idx, part; owner.partsArray) {
10533 					if(idx + 1 == owner.partsArray.length)
10534 						pos[idx] = -1;
10535 					else {
10536 						cpos += part.currentlyAssignedWidth;
10537 						pos[idx] = cpos;
10538 					}
10539 				}
10540 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
10541 			} else version(custom_widgets) {
10542 				owner.redraw();
10543 			} else static assert(false);
10544 
10545 			return p;
10546 		}
10547 	}
10548 
10549 	private Parts _parts;
10550 	///
10551 	final @property Parts parts() {
10552 		return _parts;
10553 	}
10554 
10555 	/++
10556 
10557 	+/
10558 	static class Part {
10559 		/++
10560 			History:
10561 				Added September 1, 2023 (dub v11.1)
10562 		+/
10563 		enum WidthUnits {
10564 			/++
10565 				Unscaled pixels as they appear on screen.
10566 
10567 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
10568 			+/
10569 			DeviceDependentPixels,
10570 			/++
10571 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
10572 			+/
10573 			DeviceIndependentPixels,
10574 			/++
10575 				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`).
10576 			+/
10577 			ApproximateCharacters,
10578 			/++
10579 				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.
10580 
10581 				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.
10582 			+/
10583 			Proportional
10584 		}
10585 		private WidthUnits units;
10586 		private int width;
10587 		private StatusBar owner;
10588 
10589 		private int currentlyAssignedWidth;
10590 
10591 		/++
10592 			History:
10593 				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.
10594 
10595 				It now allows you to provide your own value for [WidthUnits].
10596 
10597 				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`.
10598 		+/
10599 		this(int w, WidthUnits units = WidthUnits.Proportional) {
10600 			this.units = units;
10601 			this.width = w;
10602 		}
10603 
10604 		/// ditto
10605 		this(int w = 0) {
10606 			if(w == 0)
10607 				this(w, WidthUnits.Proportional);
10608 			else
10609 				this(w, WidthUnits.DeviceDependentPixels);
10610 		}
10611 
10612 		private int idx;
10613 		private string _content;
10614 		///
10615 		@property string content() { return _content; }
10616 		///
10617 		@property void content(string s) {
10618 			version(win32_widgets) {
10619 				_content = s;
10620 				WCharzBuffer bfr = WCharzBuffer(s);
10621 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
10622 			} else version(custom_widgets) {
10623 				if(_content != s) {
10624 					_content = s;
10625 					owner.redraw();
10626 				}
10627 			} else static assert(false);
10628 		}
10629 	}
10630 	string simpleModeContent;
10631 	bool inSimpleMode;
10632 
10633 
10634 	///
10635 	this(Widget parent) {
10636 		super(null); // FIXME
10637 		_parts = Parts(this);
10638 		tabStop = false;
10639 		version(win32_widgets) {
10640 			parentWindow = parent.parentWindow;
10641 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
10642 
10643 			RECT rect;
10644 			GetWindowRect(hwnd, &rect);
10645 			idealHeight = rect.bottom - rect.top;
10646 			assert(idealHeight);
10647 		} else version(custom_widgets) {
10648 		} else static assert(false);
10649 	}
10650 
10651 	override void recomputeChildLayout() {
10652 		int remainingLength = this.width;
10653 
10654 		int proportionalSum;
10655 		int proportionalCount;
10656 		foreach(idx, part; this.partsArray) {
10657 			with(Part.WidthUnits)
10658 			final switch(part.units) {
10659 				case DeviceDependentPixels:
10660 					part.currentlyAssignedWidth = part.width;
10661 					remainingLength -= part.currentlyAssignedWidth;
10662 				break;
10663 				case DeviceIndependentPixels:
10664 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
10665 					remainingLength -= part.currentlyAssignedWidth;
10666 				break;
10667 				case ApproximateCharacters:
10668 					auto cs = getComputedStyle();
10669 					auto font = cs.font;
10670 
10671 					part.currentlyAssignedWidth = font.averageWidth * this.width;
10672 					remainingLength -= part.currentlyAssignedWidth;
10673 				break;
10674 				case Proportional:
10675 					proportionalSum += part.width;
10676 					proportionalCount ++;
10677 				break;
10678 			}
10679 		}
10680 
10681 		foreach(part; this.partsArray) {
10682 			if(part.units == Part.WidthUnits.Proportional) {
10683 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
10684 				if(proportion == 0)
10685 					proportion = 1;
10686 
10687 				if(proportionalSum == 0)
10688 					proportionalSum = proportionalCount;
10689 
10690 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
10691 			}
10692 		}
10693 
10694 		super.recomputeChildLayout();
10695 	}
10696 
10697 	version(win32_widgets)
10698 	override protected void dpiChanged() {
10699 		RECT rect;
10700 		GetWindowRect(hwnd, &rect);
10701 		idealHeight = rect.bottom - rect.top;
10702 		assert(idealHeight);
10703 	}
10704 
10705 	version(custom_widgets)
10706 	override void paint(WidgetPainter painter) {
10707 		auto cs = getComputedStyle();
10708 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10709 		int cpos = 0;
10710 		foreach(idx, part; this.partsArray) {
10711 			auto partWidth = part.currentlyAssignedWidth;
10712 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
10713 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
10714 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
10715 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
10716 
10717 			painter.outlineColor = cs.foregroundColor();
10718 			painter.fillColor = cs.foregroundColor();
10719 
10720 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
10721 			cpos += partWidth;
10722 		}
10723 	}
10724 
10725 
10726 	version(win32_widgets) {
10727 		private int idealHeight;
10728 		override int maxHeight() { return idealHeight; }
10729 		override int minHeight() { return idealHeight; }
10730 	} else version(custom_widgets) {
10731 		override int maxHeight() { return defaultLineHeight + 4; }
10732 		override int minHeight() { return defaultLineHeight + 4; }
10733 	} else static assert(false);
10734 }
10735 
10736 /// Displays an in-progress indicator without known values
10737 version(none)
10738 class IndefiniteProgressBar : Widget {
10739 	version(win32_widgets)
10740 	this(Widget parent) {
10741 		super(parent);
10742 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
10743 		tabStop = false;
10744 	}
10745 	override int minHeight() { return 10; }
10746 }
10747 
10748 /// A progress bar with a known endpoint and completion amount
10749 class ProgressBar : Widget {
10750 	/++
10751 		History:
10752 			Added March 16, 2022 (dub v10.7)
10753 	+/
10754 	this(int min, int max, Widget parent) {
10755 		this(parent);
10756 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
10757 	}
10758 	this(Widget parent) {
10759 		version(win32_widgets) {
10760 			super(parent);
10761 			createWin32Window(this, "msctls_progress32"w, "", 0);
10762 			tabStop = false;
10763 		} else version(custom_widgets) {
10764 			super(parent);
10765 			max = 100;
10766 			step = 10;
10767 			tabStop = false;
10768 		} else static assert(0);
10769 	}
10770 
10771 	version(custom_widgets)
10772 	override void paint(WidgetPainter painter) {
10773 		auto cs = getComputedStyle();
10774 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
10775 		painter.fillColor = cs.progressBarColor;
10776 		painter.drawRectangle(Point(0, 0), width * current / max, height);
10777 	}
10778 
10779 
10780 	version(custom_widgets) {
10781 		int current;
10782 		int max;
10783 		int step;
10784 	}
10785 
10786 	///
10787 	void advanceOneStep() {
10788 		version(win32_widgets)
10789 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
10790 		else version(custom_widgets)
10791 			addToPosition(step);
10792 		else static assert(false);
10793 	}
10794 
10795 	///
10796 	void setStepIncrement(int increment) {
10797 		version(win32_widgets)
10798 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
10799 		else version(custom_widgets)
10800 			step = increment;
10801 		else static assert(false);
10802 	}
10803 
10804 	///
10805 	void addToPosition(int amount) {
10806 		version(win32_widgets)
10807 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
10808 		else version(custom_widgets)
10809 			setPosition(current + amount);
10810 		else static assert(false);
10811 	}
10812 
10813 	///
10814 	void setPosition(int pos) {
10815 		version(win32_widgets)
10816 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
10817 		else version(custom_widgets) {
10818 			current = pos;
10819 			if(current > max)
10820 				current = max;
10821 			redraw();
10822 		}
10823 		else static assert(false);
10824 	}
10825 
10826 	///
10827 	void setRange(ushort min, ushort max) {
10828 		version(win32_widgets)
10829 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
10830 		else version(custom_widgets) {
10831 			this.max = max;
10832 		}
10833 		else static assert(false);
10834 	}
10835 
10836 	override int minHeight() { return 10; }
10837 }
10838 
10839 version(custom_widgets)
10840 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
10841 	thisLabel.reserve(label.length);
10842 	bool justSawAmpersand;
10843 	foreach(ch; label) {
10844 		if(justSawAmpersand) {
10845 			justSawAmpersand = false;
10846 			if(ch == '&') {
10847 				goto plain;
10848 			}
10849 			thisAccelerator = ch;
10850 		} else {
10851 			if(ch == '&') {
10852 				justSawAmpersand = true;
10853 				continue;
10854 			}
10855 			plain:
10856 			thisLabel ~= ch;
10857 		}
10858 	}
10859 }
10860 
10861 /++
10862 	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.
10863 
10864 
10865 	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
10866 
10867 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
10868 
10869 	History:
10870 		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.
10871 +/
10872 class Fieldset : Widget {
10873 	// FIXME: on Windows,it doesn't draw the background on the label
10874 	// on X, it doesn't fix the clipping rectangle for it
10875 	version(win32_widgets)
10876 		override int paddingTop() { return defaultLineHeight; }
10877 	else version(custom_widgets)
10878 		override int paddingTop() { return defaultLineHeight + 2; }
10879 	else static assert(false);
10880 	override int paddingBottom() { return 6; }
10881 	override int paddingLeft() { return 6; }
10882 	override int paddingRight() { return 6; }
10883 
10884 	override int marginLeft() { return 6; }
10885 	override int marginRight() { return 6; }
10886 	override int marginTop() { return 2; }
10887 	override int marginBottom() { return 2; }
10888 
10889 	string legend;
10890 
10891 	version(custom_widgets) private dchar accelerator;
10892 
10893 	this(string legend, Widget parent) {
10894 		version(win32_widgets) {
10895 			super(parent);
10896 			this.legend = legend;
10897 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
10898 			tabStop = false;
10899 		} else version(custom_widgets) {
10900 			super(parent);
10901 			tabStop = false;
10902 
10903 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
10904 		} else static assert(0);
10905 	}
10906 
10907 	version(custom_widgets)
10908 	override void paint(WidgetPainter painter) {
10909 		auto dlh = defaultLineHeight;
10910 
10911 		painter.fillColor = Color.transparent;
10912 		auto cs = getComputedStyle();
10913 		painter.pen = Pen(cs.foregroundColor, 1);
10914 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
10915 
10916 		auto tx = painter.textSize(legend);
10917 		painter.outlineColor = Color.transparent;
10918 
10919 		version(Windows) {
10920 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
10921 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
10922 			SelectObject(painter.impl.hdc, b);
10923 		} else static if(UsingSimpledisplayX11) {
10924 			painter.fillColor = getComputedStyle().windowBackgroundColor;
10925 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
10926 		}
10927 		painter.outlineColor = cs.foregroundColor;
10928 		painter.drawText(Point(8, 0), legend);
10929 	}
10930 
10931 	override int maxHeight() {
10932 		auto m = paddingTop() + paddingBottom();
10933 		foreach(child; children) {
10934 			auto mh = child.maxHeight();
10935 			if(mh == int.max)
10936 				return int.max;
10937 			m += mh;
10938 			m += child.marginBottom();
10939 			m += child.marginTop();
10940 		}
10941 		m += 6;
10942 		if(m < minHeight)
10943 			return minHeight;
10944 		return m;
10945 	}
10946 
10947 	override int minHeight() {
10948 		auto m = paddingTop() + paddingBottom();
10949 		foreach(child; children) {
10950 			m += child.minHeight();
10951 			m += child.marginBottom();
10952 			m += child.marginTop();
10953 		}
10954 		return m + 6;
10955 	}
10956 
10957 	override int minWidth() {
10958 		return 6 + cast(int) this.legend.length * 7;
10959 	}
10960 }
10961 
10962 /++
10963 	$(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")
10964 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
10965 +/
10966 version(minigui_screenshots)
10967 @Screenshot("Fieldset")
10968 unittest {
10969 	auto window = new Window(200, 100);
10970 	auto set = new Fieldset("Baby will", window);
10971 	auto option1 = new Radiobox("Eat", set);
10972 	auto option2 = new Radiobox("Cry", set);
10973 	auto option3 = new Radiobox("Sleep", set);
10974 	window.loop();
10975 }
10976 
10977 /// Draws a line
10978 class HorizontalRule : Widget {
10979 	mixin Margin!q{ 2 };
10980 	override int minHeight() { return 2; }
10981 	override int maxHeight() { return 2; }
10982 
10983 	///
10984 	this(Widget parent) {
10985 		super(parent);
10986 	}
10987 
10988 	override void paint(WidgetPainter painter) {
10989 		auto cs = getComputedStyle();
10990 		painter.outlineColor = cs.darkAccentColor;
10991 		painter.drawLine(Point(0, 0), Point(width, 0));
10992 		painter.outlineColor = cs.lightAccentColor;
10993 		painter.drawLine(Point(0, 1), Point(width, 1));
10994 	}
10995 }
10996 
10997 version(minigui_screenshots)
10998 @Screenshot("HorizontalRule")
10999 /++
11000 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
11001 
11002 +/
11003 unittest {
11004 	auto window = new Window(200, 100);
11005 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
11006 	new HorizontalRule(window);
11007 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
11008 	window.loop();
11009 }
11010 
11011 /// ditto
11012 class VerticalRule : Widget {
11013 	mixin Margin!q{ 2 };
11014 	override int minWidth() { return 2; }
11015 	override int maxWidth() { return 2; }
11016 
11017 	///
11018 	this(Widget parent) {
11019 		super(parent);
11020 	}
11021 
11022 	override void paint(WidgetPainter painter) {
11023 		auto cs = getComputedStyle();
11024 		painter.outlineColor = cs.darkAccentColor;
11025 		painter.drawLine(Point(0, 0), Point(0, height));
11026 		painter.outlineColor = cs.lightAccentColor;
11027 		painter.drawLine(Point(1, 0), Point(1, height));
11028 	}
11029 }
11030 
11031 
11032 ///
11033 class Menu : Window {
11034 	void remove() {
11035 		foreach(i, child; parentWindow.children)
11036 			if(child is this) {
11037 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
11038 				break;
11039 			}
11040 		parentWindow.redraw();
11041 
11042 		parentWindow.releaseMouseCapture();
11043 	}
11044 
11045 	///
11046 	void addSeparator() {
11047 		version(win32_widgets)
11048 			AppendMenu(handle, MF_SEPARATOR, 0, null);
11049 		else version(custom_widgets)
11050 			auto hr = new HorizontalRule(this);
11051 		else static assert(0);
11052 	}
11053 
11054 	override int paddingTop() { return 4; }
11055 	override int paddingBottom() { return 4; }
11056 	override int paddingLeft() { return 2; }
11057 	override int paddingRight() { return 2; }
11058 
11059 	version(win32_widgets) {}
11060 	else version(custom_widgets) {
11061 		SimpleWindow dropDown;
11062 		Widget menuParent;
11063 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
11064 			this.menuParent = parent;
11065 
11066 			int w = 150;
11067 			int h = paddingTop + paddingBottom;
11068 			if(this.children.length) {
11069 				// hacking it to get the ideal height out of recomputeChildLayout
11070 				this.width = w;
11071 				this.height = h;
11072 				this.recomputeChildLayoutEntry();
11073 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
11074 				h += paddingBottom;
11075 
11076 				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
11077 			}
11078 
11079 			if(offsetY == int.min)
11080 				offsetY = parent.defaultLineHeight;
11081 
11082 			auto coord = parent.globalCoordinates();
11083 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
11084 			this.x = 0;
11085 			this.y = 0;
11086 			this.width = dropDown.width;
11087 			this.height = dropDown.height;
11088 			this.drawableWindow = dropDown;
11089 			this.recomputeChildLayoutEntry();
11090 
11091 			static if(UsingSimpledisplayX11)
11092 				XSync(XDisplayConnection.get, 0);
11093 
11094 			dropDown.visibilityChanged = (bool visible) {
11095 				if(visible) {
11096 					this.redraw();
11097 					dropDown.grabInput();
11098 				} else {
11099 					dropDown.releaseInputGrab();
11100 				}
11101 			};
11102 
11103 			dropDown.show();
11104 
11105 			clickListener = this.addEventListener((scope ClickEvent ev) {
11106 				unpopup();
11107 				// need to unlock asap just in case other user handlers block...
11108 				static if(UsingSimpledisplayX11)
11109 					flushGui();
11110 			}, true /* again for asap action */);
11111 		}
11112 
11113 		EventListener clickListener;
11114 	}
11115 	else static assert(false);
11116 
11117 	version(custom_widgets)
11118 	void unpopup() {
11119 		mouseLastOver = mouseLastDownOn = null;
11120 		dropDown.hide();
11121 		if(!menuParent.parentWindow.win.closed) {
11122 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
11123 				maw.setDynamicState(DynamicState.depressed, false);
11124 				maw.setDynamicState(DynamicState.hover, false);
11125 				maw.redraw();
11126 			}
11127 			// menuParent.parentWindow.win.focus();
11128 		}
11129 		clickListener.disconnect();
11130 	}
11131 
11132 	MenuItem[] items;
11133 
11134 	///
11135 	MenuItem addItem(MenuItem item) {
11136 		addChild(item);
11137 		items ~= item;
11138 		version(win32_widgets) {
11139 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11140 		}
11141 		return item;
11142 	}
11143 
11144 	string label;
11145 
11146 	version(win32_widgets) {
11147 		HMENU handle;
11148 		///
11149 		this(string label, Widget parent) {
11150 			// not actually passing the parent since it effs up the drawing
11151 			super(cast(Widget) null);// parent);
11152 			this.label = label;
11153 			handle = CreatePopupMenu();
11154 		}
11155 	} else version(custom_widgets) {
11156 		///
11157 		this(string label, Widget parent) {
11158 
11159 			if(dropDown) {
11160 				dropDown.close();
11161 			}
11162 			dropDown = new SimpleWindow(
11163 				150, 4,
11164 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
11165 
11166 			this.label = label;
11167 
11168 			super(dropDown);
11169 		}
11170 	} else static assert(false);
11171 
11172 	override int maxHeight() { return defaultLineHeight; }
11173 	override int minHeight() { return defaultLineHeight; }
11174 
11175 	version(custom_widgets)
11176 	override void paint(WidgetPainter painter) {
11177 		this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
11178 	}
11179 }
11180 
11181 /++
11182 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
11183 +/
11184 class MenuItem : MouseActivatedWidget {
11185 	Menu submenu;
11186 
11187 	Action action;
11188 	string label;
11189 
11190 	override int paddingLeft() { return 4; }
11191 
11192 	override int maxHeight() { return defaultLineHeight + 4; }
11193 	override int minHeight() { return defaultLineHeight + 4; }
11194 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
11195 	override int maxWidth() {
11196 		if(cast(MenuBar) parent) {
11197 			return minWidth();
11198 		}
11199 		return int.max;
11200 	}
11201 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
11202 	this(string lbl, Widget parent = null) {
11203 		super(parent);
11204 		//label = lbl; // FIXME
11205 		foreach(char ch; lbl) // FIXME
11206 			if(ch != '&') // FIXME
11207 				label ~= ch; // FIXME
11208 		tabStop = false; // these are selected some other way
11209 	}
11210 
11211 	///
11212 	this(Action action, Widget parent = null) {
11213 		assert(action !is null);
11214 		this(action.label, parent);
11215 		this.action = action;
11216 		tabStop = false; // these are selected some other way
11217 	}
11218 
11219 	version(custom_widgets)
11220 	override void paint(WidgetPainter painter) {
11221 		auto cs = getComputedStyle();
11222 		if(dynamicState & DynamicState.depressed)
11223 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11224 		if(dynamicState & DynamicState.hover)
11225 			painter.outlineColor = cs.activeMenuItemColor;
11226 		else
11227 			painter.outlineColor = cs.foregroundColor;
11228 		painter.fillColor = Color.transparent;
11229 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11230 		if(action && action.accelerator !is KeyEvent.init) {
11231 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
11232 
11233 		}
11234 	}
11235 
11236 	static class Style : Widget.Style {
11237 		override bool variesWithState(ulong dynamicStateFlags) {
11238 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11239 		}
11240 	}
11241 	mixin OverrideStyle!Style;
11242 
11243 	override void defaultEventHandler_triggered(Event event) {
11244 		if(action)
11245 		foreach(handler; action.triggered)
11246 			handler();
11247 
11248 		if(auto pmenu = cast(Menu) this.parent)
11249 			pmenu.remove();
11250 
11251 		super.defaultEventHandler_triggered(event);
11252 	}
11253 }
11254 
11255 version(win32_widgets)
11256 /// A "mouse activiated widget" is really just an abstract variant of button.
11257 class MouseActivatedWidget : Widget {
11258 	@property bool isChecked() {
11259 		assert(hwnd);
11260 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
11261 
11262 	}
11263 	@property void isChecked(bool state) {
11264 		assert(hwnd);
11265 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
11266 
11267 	}
11268 
11269 	override void handleWmCommand(ushort cmd, ushort id) {
11270 		if(cmd == 0) {
11271 			auto event = new Event(EventType.triggered, this);
11272 			event.dispatch();
11273 		}
11274 	}
11275 
11276 	this(Widget parent) {
11277 		super(parent);
11278 	}
11279 }
11280 else version(custom_widgets)
11281 /// ditto
11282 class MouseActivatedWidget : Widget {
11283 	@property bool isChecked() { return isChecked_; }
11284 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
11285 
11286 	private bool isChecked_;
11287 
11288 	this(Widget parent) {
11289 		super(parent);
11290 
11291 		addEventListener((MouseDownEvent ev) {
11292 			if(ev.button == MouseButton.left) {
11293 				setDynamicState(DynamicState.depressed, true);
11294 				setDynamicState(DynamicState.hover, true);
11295 				redraw();
11296 			}
11297 		});
11298 
11299 		addEventListener((MouseUpEvent ev) {
11300 			if(ev.button == MouseButton.left) {
11301 				setDynamicState(DynamicState.depressed, false);
11302 				setDynamicState(DynamicState.hover, false);
11303 				redraw();
11304 			}
11305 		});
11306 
11307 		addEventListener((MouseMoveEvent mme) {
11308 			if(!(mme.state & ModifierState.leftButtonDown)) {
11309 				if(dynamicState_ & DynamicState.depressed) {
11310 					setDynamicState(DynamicState.depressed, false);
11311 					redraw();
11312 				}
11313 			}
11314 		});
11315 	}
11316 
11317 	override void defaultEventHandler_focus(Event ev) {
11318 		super.defaultEventHandler_focus(ev);
11319 		this.redraw();
11320 	}
11321 	override void defaultEventHandler_blur(Event ev) {
11322 		super.defaultEventHandler_blur(ev);
11323 		setDynamicState(DynamicState.depressed, false);
11324 		this.redraw();
11325 	}
11326 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
11327 		super.defaultEventHandler_keydown(ev);
11328 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
11329 			setDynamicState(DynamicState.depressed, true);
11330 			setDynamicState(DynamicState.hover, true);
11331 			this.redraw();
11332 		}
11333 	}
11334 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
11335 		super.defaultEventHandler_keyup(ev);
11336 		if(!(dynamicState & DynamicState.depressed))
11337 			return;
11338 		setDynamicState(DynamicState.depressed, false);
11339 		setDynamicState(DynamicState.hover, false);
11340 		this.redraw();
11341 
11342 		auto event = new Event(EventType.triggered, this);
11343 		event.sendDirectly();
11344 	}
11345 	override void defaultEventHandler_click(ClickEvent ev) {
11346 		super.defaultEventHandler_click(ev);
11347 		if(ev.button == MouseButton.left) {
11348 			auto event = new Event(EventType.triggered, this);
11349 			event.sendDirectly();
11350 		}
11351 	}
11352 
11353 }
11354 else static assert(false);
11355 
11356 /*
11357 /++
11358 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
11359 
11360 	Basically the same as a checkbox.
11361 +/
11362 class OnOffSwitch : MouseActivatedWidget {
11363 
11364 }
11365 */
11366 
11367 /++
11368 	History:
11369 		Added June 15, 2021 (dub v10.1)
11370 +/
11371 struct ImageLabel {
11372 	/++
11373 		Defines a label+image combo used by some widgets.
11374 
11375 		If you provide just a text label, that is all the widget will try to
11376 		display. Or just an image will display just that. If you provide both,
11377 		it may display both text and image side by side or display the image
11378 		and offer text on an input event depending on the widget.
11379 
11380 		History:
11381 			The `alignment` parameter was added on September 27, 2021
11382 	+/
11383 	this(string label, TextAlignment alignment = TextAlignment.Center) {
11384 		this.label = label;
11385 		this.displayFlags = DisplayFlags.displayText;
11386 		this.alignment = alignment;
11387 	}
11388 
11389 	/// ditto
11390 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11391 		this.label = label;
11392 		this.image = image;
11393 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11394 		this.alignment = alignment;
11395 	}
11396 
11397 	/// ditto
11398 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
11399 		this.image = image;
11400 		this.displayFlags = DisplayFlags.displayImage;
11401 		this.alignment = alignment;
11402 	}
11403 
11404 	/// ditto
11405 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
11406 		this.label = label;
11407 		this.image = image;
11408 		this.alignment = alignment;
11409 		this.displayFlags = displayFlags;
11410 	}
11411 
11412 	string label;
11413 	MemoryImage image;
11414 
11415 	enum DisplayFlags {
11416 		displayText = 1 << 0,
11417 		displayImage = 1 << 1,
11418 	}
11419 
11420 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
11421 
11422 	TextAlignment alignment;
11423 }
11424 
11425 /++
11426 	A basic checked or not checked box with an attached label.
11427 
11428 
11429 	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
11430 
11431 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11432 
11433 	History:
11434 		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.
11435 +/
11436 class Checkbox : MouseActivatedWidget {
11437 	version(win32_widgets) {
11438 		override int maxHeight() { return scaleWithDpi(16); }
11439 		override int minHeight() { return scaleWithDpi(16); }
11440 	} else version(custom_widgets) {
11441 		private enum buttonSize = 16;
11442 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11443 		override int minHeight() { return maxHeight(); }
11444 	} else static assert(0);
11445 
11446 	override int marginLeft() { return 4; }
11447 
11448 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
11449 
11450 	/++
11451 		Just an alias because I keep typing checked out of web habit.
11452 
11453 		History:
11454 			Added May 31, 2021
11455 	+/
11456 	alias checked = isChecked;
11457 
11458 	private string label;
11459 	private dchar accelerator;
11460 
11461 	/++
11462 	+/
11463 	this(string label, Widget parent) {
11464 		this(ImageLabel(label), Appearance.checkbox, parent);
11465 	}
11466 
11467 	/// ditto
11468 	this(string label, Appearance appearance, Widget parent) {
11469 		this(ImageLabel(label), appearance, parent);
11470 	}
11471 
11472 	/++
11473 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
11474 
11475 		History:
11476 			Added June 29, 2021 (dub v10.2)
11477 	+/
11478 	enum Appearance {
11479 		checkbox, /// a normal checkbox
11480 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
11481 		//sliderswitch,
11482 	}
11483 	private Appearance appearance;
11484 
11485 	/// ditto
11486 	private this(ImageLabel label, Appearance appearance, Widget parent) {
11487 		super(parent);
11488 		version(win32_widgets) {
11489 			this.label = label.label;
11490 
11491 			uint extraStyle;
11492 			final switch(appearance) {
11493 				case Appearance.checkbox:
11494 				break;
11495 				case Appearance.pushbutton:
11496 					extraStyle |= BS_PUSHLIKE;
11497 				break;
11498 			}
11499 
11500 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
11501 		} else version(custom_widgets) {
11502 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
11503 		} else static assert(0);
11504 	}
11505 
11506 	version(custom_widgets)
11507 	override void paint(WidgetPainter painter) {
11508 		auto cs = getComputedStyle();
11509 		if(isFocused()) {
11510 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11511 			painter.fillColor = cs.windowBackgroundColor;
11512 			painter.drawRectangle(Point(0, 0), width, height);
11513 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11514 		} else {
11515 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
11516 			painter.fillColor = cs.windowBackgroundColor;
11517 			painter.drawRectangle(Point(0, 0), width, height);
11518 		}
11519 
11520 
11521 		painter.outlineColor = Color.black;
11522 		painter.fillColor = Color.white;
11523 		enum rectOffset = 2;
11524 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
11525 
11526 		if(isChecked) {
11527 			auto size = scaleWithDpi(2);
11528 			painter.pen = Pen(Color.black, size);
11529 			// I'm using height so the checkbox is square
11530 			enum padding = 3;
11531 			painter.drawLine(
11532 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
11533 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
11534 			);
11535 			painter.drawLine(
11536 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
11537 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
11538 			);
11539 
11540 			painter.pen = Pen(Color.black, 1);
11541 		}
11542 
11543 		if(label !is null) {
11544 			painter.outlineColor = cs.foregroundColor();
11545 			painter.fillColor = cs.foregroundColor();
11546 
11547 			// i want the centerline of the text to be aligned with the centerline of the checkbox
11548 			/+
11549 			auto font = cs.font();
11550 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
11551 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
11552 			+/
11553 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
11554 		}
11555 	}
11556 
11557 	override void defaultEventHandler_triggered(Event ev) {
11558 		isChecked = !isChecked;
11559 
11560 		this.emit!(ChangeEvent!bool)(&isChecked);
11561 
11562 		redraw();
11563 	}
11564 
11565 	/// Emits a change event with the checked state
11566 	mixin Emits!(ChangeEvent!bool);
11567 }
11568 
11569 /// Adds empty space to a layout.
11570 class VerticalSpacer : Widget {
11571 	///
11572 	this(Widget parent) {
11573 		super(parent);
11574 	}
11575 }
11576 
11577 /// ditto
11578 class HorizontalSpacer : Widget {
11579 	///
11580 	this(Widget parent) {
11581 		super(parent);
11582 		this.tabStop = false;
11583 	}
11584 }
11585 
11586 
11587 /++
11588 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
11589 
11590 
11591 	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
11592 
11593 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11594 
11595 	History:
11596 		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.
11597 +/
11598 class Radiobox : MouseActivatedWidget {
11599 
11600 	version(win32_widgets) {
11601 		override int maxHeight() { return scaleWithDpi(16); }
11602 		override int minHeight() { return scaleWithDpi(16); }
11603 	} else version(custom_widgets) {
11604 		private enum buttonSize = 16;
11605 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
11606 		override int minHeight() { return maxHeight(); }
11607 	} else static assert(0);
11608 
11609 	override int marginLeft() { return 4; }
11610 
11611 	// FIXME: make a label getter
11612 	private string label;
11613 	private dchar accelerator;
11614 
11615 	/++
11616 
11617 	+/
11618 	this(string label, Widget parent) {
11619 		super(parent);
11620 		version(win32_widgets) {
11621 			this.label = label;
11622 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
11623 		} else version(custom_widgets) {
11624 			label.extractWindowsStyleLabel(this.label, this.accelerator);
11625 			height = 16;
11626 			width = height + 4 + cast(int) label.length * 16;
11627 		}
11628 	}
11629 
11630 	version(custom_widgets)
11631 	override void paint(WidgetPainter painter) {
11632 		auto cs = getComputedStyle();
11633 
11634 		if(isFocused) {
11635 			painter.fillColor = cs.windowBackgroundColor;
11636 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
11637 		} else {
11638 			painter.fillColor = cs.windowBackgroundColor;
11639 			painter.outlineColor = cs.windowBackgroundColor;
11640 		}
11641 		painter.drawRectangle(Point(0, 0), width, height);
11642 
11643 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
11644 
11645 		painter.outlineColor = Color.black;
11646 		painter.fillColor = Color.white;
11647 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
11648 		if(isChecked) {
11649 			painter.outlineColor = Color.black;
11650 			painter.fillColor = Color.black;
11651 			// I'm using height so the checkbox is square
11652 			auto size = scaleWithDpi(2);
11653 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
11654 		}
11655 
11656 		painter.outlineColor = cs.foregroundColor();
11657 		painter.fillColor = cs.foregroundColor();
11658 
11659 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
11660 	}
11661 
11662 
11663 	override void defaultEventHandler_triggered(Event ev) {
11664 		isChecked = true;
11665 
11666 		if(this.parent) {
11667 			foreach(child; this.parent.children) {
11668 				if(child is this) continue;
11669 				if(auto rb = cast(Radiobox) child) {
11670 					rb.isChecked = false;
11671 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
11672 					rb.redraw();
11673 				}
11674 			}
11675 		}
11676 
11677 		this.emit!(ChangeEvent!bool)(&this.isChecked);
11678 
11679 		redraw();
11680 	}
11681 
11682 	/// 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.
11683 	mixin Emits!(ChangeEvent!bool);
11684 }
11685 
11686 
11687 /++
11688 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
11689 
11690 
11691 	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
11692 
11693 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11694 
11695 	History:
11696 		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.
11697 +/
11698 class Button : MouseActivatedWidget {
11699 	override int heightStretchiness() { return 3; }
11700 	override int widthStretchiness() { return 3; }
11701 
11702 	/++
11703 		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.
11704 
11705 		History:
11706 			Added July 2, 2021
11707 	+/
11708 	public bool triggersOnMultiClick;
11709 
11710 	private string label_;
11711 	private TextAlignment alignment;
11712 	private dchar accelerator;
11713 
11714 	///
11715 	string label() { return label_; }
11716 	///
11717 	void label(string l) {
11718 		label_ = l;
11719 		version(win32_widgets) {
11720 			WCharzBuffer bfr = WCharzBuffer(l);
11721 			SetWindowTextW(hwnd, bfr.ptr);
11722 		} else version(custom_widgets) {
11723 			redraw();
11724 		}
11725 	}
11726 
11727 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
11728 		super.defaultEventHandler_dblclick(ev);
11729 		if(triggersOnMultiClick) {
11730 			if(ev.button == MouseButton.left) {
11731 				auto event = new Event(EventType.triggered, this);
11732 				event.sendDirectly();
11733 			}
11734 		}
11735 	}
11736 
11737 	private Sprite sprite;
11738 	private int displayFlags;
11739 
11740 	/++
11741 		Creates a push button with the given label, which may be an image or some text.
11742 
11743 		Bugs:
11744 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
11745 
11746 		History:
11747 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
11748 
11749 			The button with label and image will respect requests to show both on Windows as
11750 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
11751 	+/
11752 	this(ImageLabel label, Widget parent) {
11753 		version(win32_widgets) {
11754 			// FIXME: use ideal button size instead
11755 			width = 50;
11756 			height = 30;
11757 			super(parent);
11758 
11759 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
11760 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
11761 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
11762 
11763 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
11764 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
11765 
11766 			if(label.image) {
11767 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
11768 
11769 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
11770 			}
11771 
11772 			this.label = label.label;
11773 		} else version(custom_widgets) {
11774 			width = 50;
11775 			height = 30;
11776 			super(parent);
11777 
11778 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
11779 
11780 			if(label.image) {
11781 				this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
11782 				this.displayFlags = label.displayFlags;
11783 			}
11784 
11785 			this.alignment = label.alignment;
11786 		}
11787 	}
11788 
11789 	///
11790 	this(string label, Widget parent) {
11791 		this(ImageLabel(label), parent);
11792 	}
11793 
11794 	override int minHeight() { return defaultLineHeight + 4; }
11795 
11796 	static class Style : Widget.Style {
11797 		override WidgetBackground background() {
11798 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
11799 
11800 			auto pressed = DynamicState.depressed | DynamicState.hover;
11801 			if((widget.dynamicState & pressed) == pressed) {
11802 				return WidgetBackground(cs.depressedButtonColor());
11803 			} else if(widget.dynamicState & DynamicState.hover) {
11804 				return WidgetBackground(cs.hoveringColor());
11805 			} else {
11806 				return WidgetBackground(cs.buttonColor());
11807 			}
11808 		}
11809 
11810 		override FrameStyle borderStyle() {
11811 			auto pressed = DynamicState.depressed | DynamicState.hover;
11812 			if((widget.dynamicState & pressed) == pressed) {
11813 				return FrameStyle.sunk;
11814 			} else {
11815 				return FrameStyle.risen;
11816 			}
11817 
11818 		}
11819 
11820 		override bool variesWithState(ulong dynamicStateFlags) {
11821 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
11822 		}
11823 	}
11824 	mixin OverrideStyle!Style;
11825 
11826 	version(custom_widgets)
11827 	override void paint(WidgetPainter painter) {
11828 		painter.drawThemed(delegate Rectangle(const Rectangle bounds) {
11829 			if(sprite) {
11830 				sprite.drawAt(
11831 					painter,
11832 					bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
11833 					Point(0, 0)
11834 				);
11835 			} else {
11836 				painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
11837 			}
11838 			return bounds;
11839 		});
11840 	}
11841 
11842 	override int flexBasisWidth() {
11843 		version(win32_widgets) {
11844 			SIZE size;
11845 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11846 			if(size.cx == 0)
11847 				goto fallback;
11848 			return size.cx + scaleWithDpi(16);
11849 		}
11850 		fallback:
11851 			return scaleWithDpi(cast(int) label.length * 8 + 16);
11852 	}
11853 
11854 	override int flexBasisHeight() {
11855 		version(win32_widgets) {
11856 			SIZE size;
11857 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
11858 			if(size.cy == 0)
11859 				goto fallback;
11860 			return size.cy + scaleWithDpi(6);
11861 		}
11862 		fallback:
11863 			return defaultLineHeight + 4;
11864 	}
11865 }
11866 
11867 /++
11868 	A button with a consistent size, suitable for user commands like OK and CANCEL.
11869 +/
11870 class CommandButton : Button {
11871 	this(string label, Widget parent) {
11872 		super(label, parent);
11873 	}
11874 
11875 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
11876 
11877 	override int maxHeight() {
11878 		return defaultLineHeight + 4;
11879 	}
11880 
11881 	override int maxWidth() {
11882 		return defaultLineHeight * 4;
11883 	}
11884 
11885 	override int marginLeft() { return 12; }
11886 	override int marginRight() { return 12; }
11887 	override int marginTop() { return 12; }
11888 	override int marginBottom() { return 12; }
11889 }
11890 
11891 ///
11892 enum ArrowDirection {
11893 	left, ///
11894 	right, ///
11895 	up, ///
11896 	down ///
11897 }
11898 
11899 ///
11900 version(custom_widgets)
11901 class ArrowButton : Button {
11902 	///
11903 	this(ArrowDirection direction, Widget parent) {
11904 		super("", parent);
11905 		this.direction = direction;
11906 		triggersOnMultiClick = true;
11907 	}
11908 
11909 	private ArrowDirection direction;
11910 
11911 	override int minHeight() { return scaleWithDpi(16); }
11912 	override int maxHeight() { return scaleWithDpi(16); }
11913 	override int minWidth() { return scaleWithDpi(16); }
11914 	override int maxWidth() { return scaleWithDpi(16); }
11915 
11916 	override void paint(WidgetPainter painter) {
11917 		super.paint(painter);
11918 
11919 		auto cs = getComputedStyle();
11920 
11921 		painter.outlineColor = cs.foregroundColor;
11922 		painter.fillColor = cs.foregroundColor;
11923 
11924 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
11925 
11926 		final switch(direction) {
11927 			case ArrowDirection.up:
11928 				painter.drawPolygon(
11929 					scaleWithDpi(Point(2, 10) + offset),
11930 					scaleWithDpi(Point(7, 5) + offset),
11931 					scaleWithDpi(Point(12, 10) + offset),
11932 					scaleWithDpi(Point(2, 10) + offset)
11933 				);
11934 			break;
11935 			case ArrowDirection.down:
11936 				painter.drawPolygon(
11937 					scaleWithDpi(Point(2, 6) + offset),
11938 					scaleWithDpi(Point(7, 11) + offset),
11939 					scaleWithDpi(Point(12, 6) + offset),
11940 					scaleWithDpi(Point(2, 6) + offset)
11941 				);
11942 			break;
11943 			case ArrowDirection.left:
11944 				painter.drawPolygon(
11945 					scaleWithDpi(Point(10, 2) + offset),
11946 					scaleWithDpi(Point(5, 7) + offset),
11947 					scaleWithDpi(Point(10, 12) + offset),
11948 					scaleWithDpi(Point(10, 2) + offset)
11949 				);
11950 			break;
11951 			case ArrowDirection.right:
11952 				painter.drawPolygon(
11953 					scaleWithDpi(Point(6, 2) + offset),
11954 					scaleWithDpi(Point(11, 7) + offset),
11955 					scaleWithDpi(Point(6, 12) + offset),
11956 					scaleWithDpi(Point(6, 2) + offset)
11957 				);
11958 			break;
11959 		}
11960 	}
11961 }
11962 
11963 private
11964 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
11965 	int x, y;
11966 	Widget par = c;
11967 	while(par) {
11968 		x += par.x;
11969 		y += par.y;
11970 		par = par.parent;
11971 	}
11972 	return [x, y];
11973 }
11974 
11975 version(win32_widgets)
11976 private
11977 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
11978 // MapWindowPoints?
11979 	int x, y;
11980 	Widget par = c;
11981 	while(par) {
11982 		x += par.x;
11983 		y += par.y;
11984 		par = par.parent;
11985 		if(par !is null && par.useNativeDrawing())
11986 			break;
11987 	}
11988 	return [x, y];
11989 }
11990 
11991 ///
11992 class ImageBox : Widget {
11993 	private MemoryImage image_;
11994 
11995 	override int widthStretchiness() { return 1; }
11996 	override int heightStretchiness() { return 1; }
11997 	override int widthShrinkiness() { return 1; }
11998 	override int heightShrinkiness() { return 1; }
11999 
12000 	override int flexBasisHeight() {
12001 		return image_.height;
12002 	}
12003 
12004 	override int flexBasisWidth() {
12005 		return image_.width;
12006 	}
12007 
12008 	///
12009 	public void setImage(MemoryImage image){
12010 		this.image_ = image;
12011 		if(this.parentWindow && this.parentWindow.win) {
12012 			if(sprite)
12013 				sprite.dispose();
12014 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
12015 		}
12016 		redraw();
12017 	}
12018 
12019 	/// How to fit the image in the box if they aren't an exact match in size?
12020 	enum HowToFit {
12021 		center, /// centers the image, cropping around all the edges as needed
12022 		crop, /// always draws the image in the upper left, cropping the lower right if needed
12023 		// stretch, /// not implemented
12024 	}
12025 
12026 	private Sprite sprite;
12027 	private HowToFit howToFit_;
12028 
12029 	private Color backgroundColor_;
12030 
12031 	///
12032 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
12033 		this.image_ = image;
12034 		this.tabStop = false;
12035 		this.howToFit_ = howToFit;
12036 		this.backgroundColor_ = backgroundColor;
12037 		super(parent);
12038 		updateSprite();
12039 	}
12040 
12041 	/// ditto
12042 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
12043 		this(image, howToFit, Color.transparent, parent);
12044 	}
12045 
12046 	private void updateSprite() {
12047 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
12048 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
12049 		}
12050 	}
12051 
12052 	override void paint(WidgetPainter painter) {
12053 		updateSprite();
12054 		if(backgroundColor_.a) {
12055 			painter.fillColor = backgroundColor_;
12056 			painter.drawRectangle(Point(0, 0), width, height);
12057 		}
12058 		if(howToFit_ == HowToFit.crop)
12059 			sprite.drawAt(painter, Point(0, 0));
12060 		else if(howToFit_ == HowToFit.center) {
12061 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
12062 		}
12063 	}
12064 }
12065 
12066 ///
12067 class TextLabel : Widget {
12068 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
12069 	override int maxHeight() { return minHeight; }
12070 	override int minWidth() { return 32; }
12071 
12072 	override int flexBasisHeight() { return minHeight(); }
12073 	override int flexBasisWidth() { return defaultTextWidth(label); }
12074 
12075 	string label_;
12076 
12077 	/++
12078 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
12079 
12080 		In practice this means a click on the label will focus the `labelFor`. In future versions
12081 		it will also set screen reader hints but that is not yet implemented.
12082 
12083 		History:
12084 			Added October 3, 2021 (dub v10.4)
12085 	+/
12086 	Widget labelFor;
12087 
12088 	///
12089 	@scriptable
12090 	string label() { return label_; }
12091 
12092 	///
12093 	@scriptable
12094 	void label(string l) {
12095 		label_ = l;
12096 		version(win32_widgets) {
12097 			WCharzBuffer bfr = WCharzBuffer(l);
12098 			SetWindowTextW(hwnd, bfr.ptr);
12099 		} else version(custom_widgets)
12100 			redraw();
12101 	}
12102 
12103 	override void defaultEventHandler_click(scope ClickEvent ce) {
12104 		if(this.labelFor !is null)
12105 			this.labelFor.focus();
12106 	}
12107 
12108 	/++
12109 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
12110 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
12111 	+/
12112 	this(string label, TextAlignment alignment, Widget parent) {
12113 		this.label_ = label;
12114 		this.alignment = alignment;
12115 		this.tabStop = false;
12116 		super(parent);
12117 
12118 		version(win32_widgets)
12119 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
12120 	}
12121 
12122 	/// ditto
12123 	this(string label, Widget parent) {
12124 		this(label, TextAlignment.Right, parent);
12125 	}
12126 
12127 	TextAlignment alignment;
12128 
12129 	version(custom_widgets)
12130 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
12131 		painter.outlineColor = getComputedStyle().foregroundColor;
12132 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
12133 		return bounds;
12134 	}
12135 
12136 }
12137 
12138 version(custom_widgets)
12139 	private struct etc {
12140 		mixin ExperimentalTextComponent;
12141 	}
12142 
12143 version(win32_widgets) {
12144 	alias EditableTextWidgetParent = Widget; ///
12145 	version=use_new_text_system;
12146 	import arsd.textlayouter;
12147 } else version(custom_widgets) {
12148 	version(trash_text) {
12149 		alias EditableTextWidgetParent = ScrollableWidget; ///
12150 	} else {
12151 		alias EditableTextWidgetParent = Widget;
12152 		version=use_new_text_system;
12153 		import arsd.textlayouter;
12154 	}
12155 } else static assert(0);
12156 
12157 version(use_new_text_system)
12158 class TextDisplayHelper : Widget {
12159 	protected TextLayouter l;
12160 	protected ScrollMessageWidget smw;
12161 
12162 	private const(TextLayouter.State)*[] undoStack;
12163 	private const(TextLayouter.State)*[] redoStack;
12164 
12165 	private string preservedPrimaryText;
12166 	protected void selectionChanged() {
12167 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
12168 		static if(UsingSimpledisplayX11)
12169 		with(l.selection()) {
12170 			if(!isEmpty()) {
12171 				//sdpyPrintDebugString("!isEmpty");
12172 
12173 				getPrimarySelection(parentWindow.win, (in char[] txt) {
12174 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
12175 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
12176 					if(txt.length) {
12177 						preservedPrimaryText = txt.idup;
12178 						// writeln(preservedPrimaryText);
12179 					}
12180 
12181 					setPrimarySelection(parentWindow.win, getContentString());
12182 				});
12183 			}
12184 		}
12185 	}
12186 
12187 	final TextLayouter layouter() {
12188 		return l;
12189 	}
12190 
12191 	bool readonly;
12192 	bool caretNavigation; // scroll lock can flip this
12193 	bool singleLine;
12194 	bool acceptsTabInput;
12195 
12196 	private Menu ctx;
12197 	override Menu contextMenu(int x, int y) {
12198 		if(ctx is null) {
12199 			ctx = new Menu("Actions", this);
12200 			if(!readonly) {
12201 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
12202 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
12203 				ctx.addSeparator();
12204 			}
12205 			if(!readonly)
12206 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
12207 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
12208 			if(!readonly)
12209 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
12210 			if(!readonly)
12211 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
12212 			ctx.addSeparator();
12213 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
12214 		}
12215 		return ctx;
12216 	}
12217 
12218 	override void defaultEventHandler_blur(Event ev) {
12219 		super.defaultEventHandler_blur(ev);
12220 		if(l.wasMutated()) {
12221 			auto evt = new ChangeEvent!string(this, &this.content);
12222 			evt.dispatch();
12223 			l.clearWasMutatedFlag();
12224 		}
12225 	}
12226 
12227 	private string content() {
12228 		return l.getTextString();
12229 	}
12230 
12231 	void undo() {
12232 		if(readonly) return;
12233 		if(undoStack.length) {
12234 			auto state = undoStack[$-1];
12235 			undoStack = undoStack[0 .. $-1];
12236 			undoStack.assumeSafeAppend();
12237 			redoStack ~= l.saveState();
12238 			l.restoreState(state);
12239 			adjustScrollbarSizes();
12240 			scrollForCaret();
12241 			redraw();
12242 			stateCheckpoint = true;
12243 		}
12244 	}
12245 
12246 	void redo() {
12247 		if(readonly) return;
12248 		if(redoStack.length) {
12249 			doStateCheckpoint();
12250 			auto state = redoStack[$-1];
12251 			redoStack = redoStack[0 .. $-1];
12252 			redoStack.assumeSafeAppend();
12253 			l.restoreState(state);
12254 			adjustScrollbarSizes();
12255 			scrollForCaret();
12256 			redraw();
12257 			stateCheckpoint = true;
12258 		}
12259 	}
12260 
12261 	void cut() {
12262 		if(readonly) return;
12263 		with(l.selection()) {
12264 			if(!isEmpty()) {
12265 				setClipboardText(parentWindow.win, getContentString());
12266 				doStateCheckpoint();
12267 				replaceContent("");
12268 				adjustScrollbarSizes();
12269 				scrollForCaret();
12270 				this.redraw();
12271 			}
12272 		}
12273 
12274 	}
12275 
12276 	void copy() {
12277 		with(l.selection()) {
12278 			if(!isEmpty()) {
12279 				setClipboardText(parentWindow.win, getContentString());
12280 				this.redraw();
12281 			}
12282 		}
12283 	}
12284 
12285 	void paste() {
12286 		if(readonly) return;
12287 		getClipboardText(parentWindow.win, (txt) {
12288 			doStateCheckpoint();
12289 			if(singleLine)
12290 				l.selection.replaceContent(txt.stripInternal());
12291 			else
12292 				l.selection.replaceContent(txt);
12293 			adjustScrollbarSizes();
12294 			scrollForCaret();
12295 			this.redraw();
12296 		});
12297 	}
12298 
12299 	void deleteContentOfSelection() {
12300 		if(readonly) return;
12301 		doStateCheckpoint();
12302 		l.selection.replaceContent("");
12303 		l.selection.setUserXCoordinate();
12304 		adjustScrollbarSizes();
12305 		scrollForCaret();
12306 		redraw();
12307 	}
12308 
12309 	void selectAll() {
12310 		with(l.selection) {
12311 			moveToStartOfDocument();
12312 			setAnchor();
12313 			moveToEndOfDocument();
12314 			setFocus();
12315 
12316 			selectionChanged();
12317 		}
12318 		redraw();
12319 	}
12320 
12321 	protected bool stateCheckpoint = true;
12322 
12323 	protected void doStateCheckpoint() {
12324 		if(stateCheckpoint) {
12325 			undoStack ~= l.saveState();
12326 			stateCheckpoint = false;
12327 		}
12328 	}
12329 
12330 	protected void adjustScrollbarSizes() {
12331 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
12332 		auto borderWidth = 2;
12333 		this.smw.setTotalArea(l.width, l.height);
12334 		this.smw.setViewableArea(
12335 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
12336 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
12337 	}
12338 
12339 	protected void scrollForCaret() {
12340 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
12341 		smw.scrollIntoView(l.selection.focusBoundingBox());
12342 	}
12343 
12344 	// FIXME: this should be a theme changed event listener instead
12345 	private BaseVisualTheme currentTheme;
12346 	override void recomputeChildLayout() {
12347 		if(currentTheme is null)
12348 			currentTheme = WidgetPainter.visualTheme;
12349 		if(WidgetPainter.visualTheme !is currentTheme) {
12350 			currentTheme = WidgetPainter.visualTheme;
12351 			auto ds = this.l.defaultStyle;
12352 			if(auto ms = cast(MyTextStyle) ds) {
12353 				auto cs = getComputedStyle();
12354 				auto font = cs.font();
12355 				if(font !is null)
12356 					ms.font_ = font;
12357 				else {
12358 					auto osc = new OperatingSystemFont();
12359 					osc.loadDefault;
12360 					ms.font_ = osc;
12361 				}
12362 			}
12363 		}
12364 		super.recomputeChildLayout();
12365 	}
12366 
12367 	private Point adjustForSingleLine(Point p) {
12368 		if(singleLine)
12369 			return Point(p.x, this.height / 2);
12370 		else
12371 			return p;
12372 	}
12373 
12374 	private bool wordWrapEnabled_;
12375 
12376 	this(TextLayouter l, ScrollMessageWidget parent) {
12377 		this.smw = parent;
12378 
12379 		smw.addDefaultWheelListeners(16, 16, 8);
12380 		smw.movementPerButtonClick(16, 16);
12381 
12382 		this.defaultPadding = Rectangle(2, 2, 2, 2);
12383 
12384 		this.l = l;
12385 		super(parent);
12386 
12387 		smw.addEventListener((scope ScrollEvent se) {
12388 			this.redraw();
12389 		});
12390 
12391 		bool mouseDown;
12392 		bool mouseActuallyMoved;
12393 
12394 		this.addEventListener((scope ResizeEvent re) {
12395 			// FIXME: I should add a method to give this client area width thing
12396 			if(wordWrapEnabled_)
12397 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
12398 
12399 			adjustScrollbarSizes();
12400 			scrollForCaret();
12401 
12402 			this.redraw();
12403 		});
12404 
12405 		this.addEventListener((scope KeyDownEvent kde) {
12406 			switch(kde.key) {
12407 				case Key.Up, Key.Down, Key.Left, Key.Right:
12408 				case Key.Home, Key.End:
12409 					stateCheckpoint = true;
12410 					bool setPosition = false;
12411 					switch(kde.key) {
12412 						case Key.Up: l.selection.moveUp(); break;
12413 						case Key.Down: l.selection.moveDown(); break;
12414 						case Key.Left: l.selection.moveLeft(); setPosition = true; break;
12415 						case Key.Right: l.selection.moveRight(); setPosition = true; break;
12416 						case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
12417 						case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
12418 						default: assert(0);
12419 					}
12420 
12421 					if(kde.shiftKey)
12422 						l.selection.setFocus();
12423 					else
12424 						l.selection.setAnchor();
12425 
12426 					selectionChanged();
12427 
12428 					if(setPosition)
12429 						l.selection.setUserXCoordinate();
12430 					scrollForCaret();
12431 					redraw();
12432 				break;
12433 				case Key.PageUp, Key.PageDown:
12434 					// FIXME
12435 					scrollForCaret();
12436 				break;
12437 				case Key.Delete:
12438 					if(l.selection.isEmpty()) {
12439 						l.selection.setAnchor();
12440 						l.selection.moveRight();
12441 						l.selection.setFocus();
12442 					}
12443 					deleteContentOfSelection();
12444 					adjustScrollbarSizes();
12445 					scrollForCaret();
12446 				break;
12447 				case Key.Insert:
12448 				break;
12449 				case Key.A:
12450 					if(kde.ctrlKey)
12451 						selectAll();
12452 				break;
12453 				case Key.F:
12454 					// find
12455 				break;
12456 				case Key.Z:
12457 					if(kde.ctrlKey)
12458 						undo();
12459 				break;
12460 				case Key.R:
12461 					if(kde.ctrlKey)
12462 						redo();
12463 				break;
12464 				case Key.X:
12465 					if(kde.ctrlKey)
12466 						cut();
12467 				break;
12468 				case Key.C:
12469 					if(kde.ctrlKey)
12470 						copy();
12471 				break;
12472 				case Key.V:
12473 					if(kde.ctrlKey)
12474 						paste();
12475 				break;
12476 				case Key.F1:
12477 					with(l.selection()) {
12478 						moveToStartOfLine();
12479 						setAnchor();
12480 						moveToEndOfLine();
12481 						moveToIncludeAdjacentEndOfLineMarker();
12482 						setFocus();
12483 						replaceContent("");
12484 					}
12485 
12486 					redraw();
12487 				break;
12488 				/*
12489 				case Key.F2:
12490 					l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
12491 						//(cast(MyTextStyle) old).font,
12492 						font2,
12493 						Color.red)));
12494 					redraw();
12495 				break;
12496 				*/
12497 				case Key.Tab:
12498 					// we process the char event, so don't want to change focus on it
12499 					if(acceptsTabInput)
12500 						kde.preventDefault();
12501 				break;
12502 				default:
12503 			}
12504 		});
12505 
12506 		Point downAt;
12507 
12508 		static if(UsingSimpledisplayX11)
12509 		this.addEventListener((scope ClickEvent ce) {
12510 			if(ce.button == MouseButton.middle) {
12511 				parentWindow.win.getPrimarySelection((txt) {
12512 					doStateCheckpoint();
12513 
12514 					// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
12515 
12516 					if(txt == l.selection.getContentString && preservedPrimaryText.length)
12517 						l.selection.replaceContent(preservedPrimaryText);
12518 					else
12519 						l.selection.replaceContent(txt);
12520 					redraw();
12521 				});
12522 			}
12523 		});
12524 
12525 		this.addEventListener((scope DoubleClickEvent dce) {
12526 			if(dce.button == MouseButton.left) {
12527 				with(l.selection()) {
12528 					scope dg = delegate const(char)[] (scope return const(char)[] ch) {
12529 						if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
12530 							return ch;
12531 						return null;
12532 					};
12533 					find(dg, 1, true).moveToEnd.setAnchor;
12534 					find(dg, 1, false).moveTo.setFocus;
12535 					selectionChanged();
12536 					redraw();
12537 				}
12538 			}
12539 		});
12540 
12541 		this.addEventListener((scope MouseDownEvent ce) {
12542 			if(ce.button == MouseButton.left) {
12543 				downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
12544 				l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
12545 				l.selection.setAnchor();
12546 				mouseDown = true;
12547 				mouseActuallyMoved = false;
12548 				parentWindow.captureMouse(this);
12549 				this.redraw();
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, null, 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 /// Group: generating_from_code
15790 enum context_menu = menu.init;
15791 
15792 
15793 /++
15794 	Observes and allows inspection of an object via automatic gui
15795 +/
15796 /// Group: generating_from_code
15797 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
15798 	return new ObjectInspectionWindowImpl!(T)(t);
15799 }
15800 
15801 class ObjectInspectionWindow : Window {
15802 	this(int a, int b, string c) {
15803 		super(a, b, c);
15804 	}
15805 
15806 	abstract void readUpdatesFromObject();
15807 }
15808 
15809 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
15810 	T t;
15811 	this(T t) {
15812 		this.t = t;
15813 
15814 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
15815 
15816 		foreach(memberName; __traits(derivedMembers, T)) {{
15817 			alias member = I!(__traits(getMember, t, memberName))[0];
15818 			alias type = typeof(member);
15819 			static if(is(type == int)) {
15820 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
15821 				//le.addEventListener("char", (Event ev) {
15822 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
15823 						//ev.preventDefault();
15824 				//});
15825 				le.addEventListener(EventType.change, (Event ev) {
15826 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
15827 				});
15828 
15829 				updateMemberDelegates[memberName] = () {
15830 					le.content = toInternal!string(__traits(getMember, t, memberName));
15831 				};
15832 			}
15833 		}}
15834 	}
15835 
15836 	void delegate()[string] updateMemberDelegates;
15837 
15838 	override void readUpdatesFromObject() {
15839 		foreach(k, v; updateMemberDelegates)
15840 			v();
15841 	}
15842 }
15843 
15844 /++
15845 	Creates a dialog based on a data structure.
15846 
15847 	---
15848 	dialog(window, (YourStructure value) {
15849 		// the user filled in the struct and clicked OK,
15850 		// you can check the members now
15851 	});
15852 	---
15853 
15854 	Params:
15855 		initialData = the initial value to show in the dialog. It will not modify this unless
15856 		it is a class then it might, no promises.
15857 
15858 	History:
15859 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
15860 
15861 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
15862 		be deprecated soon.
15863 +/
15864 /// Group: generating_from_code
15865 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15866 	dialog(null, T.init, onOK, onCancel, title);
15867 }
15868 /// ditto
15869 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15870 	dialog(null, T.init, onOK, onCancel, title);
15871 }
15872 /// ditto
15873 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15874 	dialog(parent, T.init, onOK, onCancel, title);
15875 }
15876 /// ditto
15877 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15878 	dialog(parent, initialData, onOK, onCancel, title);
15879 }
15880 /// ditto
15881 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
15882 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
15883 	dg.show();
15884 }
15885 
15886 private static template I(T...) { alias I = T; }
15887 
15888 
15889 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
15890 	if(name == "id")
15891 		return allLowerCase ? name : "ID";
15892 
15893 	char[160] buffer;
15894 	int bufferIndex = 0;
15895 	bool shouldCap = true;
15896 	bool shouldSpace;
15897 	bool lastWasCap;
15898 	foreach(idx, char ch; name) {
15899 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15900 
15901 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
15902 			if(lastWasCap) {
15903 				// two caps in a row, don't change. Prolly acronym.
15904 			} else {
15905 				if(idx)
15906 					shouldSpace = true; // new word, add space
15907 			}
15908 
15909 			lastWasCap = true;
15910 		} else {
15911 			lastWasCap = false;
15912 		}
15913 
15914 		if(shouldSpace) {
15915 			buffer[bufferIndex++] = space;
15916 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
15917 			shouldSpace = false;
15918 		}
15919 		if(shouldCap) {
15920 			if(ch >= 'a' && ch <= 'z')
15921 				ch -= 32;
15922 			shouldCap = false;
15923 		}
15924 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
15925 			ch += 32;
15926 		buffer[bufferIndex++] = ch;
15927 	}
15928 	return buffer[0 .. bufferIndex].idup;
15929 }
15930 
15931 /++
15932 	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.
15933 +/
15934 class AutomaticDialog(T) : Dialog {
15935 	T t;
15936 
15937 	void delegate(T) onOK;
15938 	void delegate() onCancel;
15939 
15940 	override int paddingTop() { return defaultLineHeight; }
15941 	override int paddingBottom() { return defaultLineHeight; }
15942 	override int paddingRight() { return defaultLineHeight; }
15943 	override int paddingLeft() { return defaultLineHeight; }
15944 
15945 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
15946 		assert(onOK !is null);
15947 
15948 		t = initialData;
15949 
15950 		static if(is(T == class)) {
15951 			if(t is null)
15952 				t = new T();
15953 		}
15954 		this.onOK = onOK;
15955 		this.onCancel = onCancel;
15956 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
15957 
15958 		static if(is(T == class))
15959 			this.addDataControllerWidget(t);
15960 		else
15961 			this.addDataControllerWidget(&t);
15962 
15963 		auto hl = new HorizontalLayout(this);
15964 		auto stretch = new HorizontalSpacer(hl); // to right align
15965 		auto ok = new CommandButton("OK", hl);
15966 		auto cancel = new CommandButton("Cancel", hl);
15967 		ok.addEventListener(EventType.triggered, &OK);
15968 		cancel.addEventListener(EventType.triggered, &Cancel);
15969 
15970 		this.addEventListener((KeyDownEvent ev) {
15971 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
15972 				ok.focus();
15973 				OK();
15974 				ev.preventDefault();
15975 			}
15976 			if(ev.key == Key.Escape) {
15977 				Cancel();
15978 				ev.preventDefault();
15979 			}
15980 		});
15981 
15982 		this.addEventListener((scope ClosedEvent ce) {
15983 			if(onCancel)
15984 				onCancel();
15985 		});
15986 
15987 		//this.children[0].focus();
15988 	}
15989 
15990 	override void OK() {
15991 		onOK(t);
15992 		close();
15993 	}
15994 
15995 	override void Cancel() {
15996 		if(onCancel)
15997 			onCancel();
15998 		close();
15999 	}
16000 }
16001 
16002 private template baseClassCount(Class) {
16003 	private int helper() {
16004 		int count = 0;
16005 		static if(is(Class bases == super)) {
16006 			foreach(base; bases)
16007 				static if(is(base == class))
16008 					count += 1 + baseClassCount!base;
16009 		}
16010 		return count;
16011 	}
16012 
16013 	enum int baseClassCount = helper();
16014 }
16015 
16016 private long stringToLong(string s) {
16017 	long ret;
16018 	if(s.length == 0)
16019 		return ret;
16020 	bool negative = s[0] == '-';
16021 	if(negative)
16022 		s = s[1 .. $];
16023 	foreach(ch; s) {
16024 		if(ch >= '0' && ch <= '9') {
16025 			ret *= 10;
16026 			ret += ch - '0';
16027 		}
16028 	}
16029 	if(negative)
16030 		ret = -ret;
16031 	return ret;
16032 }
16033 
16034 
16035 interface ReflectableProperties {
16036 	/++
16037 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
16038 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
16039 		json in the current implementation.
16040 
16041 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
16042 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
16043 		as of the June 2, 2021 release.
16044 
16045 		History:
16046 			Added June 2, 2021.
16047 
16048 		See_Also: [getPropertyAsString], [setPropertyFromString]
16049 	+/
16050 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
16051 	/++
16052 		Requests a property to be delivered to you as a string, through your `sink` delegate.
16053 
16054 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
16055 		be interpreted as json, otherwise, it is just a plain string.
16056 
16057 		The sink should always be called exactly once for each call (it is basically a return value, but it might
16058 		use a local buffer it maintains instead of allocating a return value).
16059 
16060 		History:
16061 			Added June 2, 2021.
16062 
16063 		See_Also: [getPropertiesList], [setPropertyFromString]
16064 	+/
16065 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
16066 	/++
16067 		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.
16068 
16069 		History:
16070 			Added June 2, 2021.
16071 
16072 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
16073 	+/
16074 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
16075 
16076 	/// [setPropertyFromString] possible return values
16077 	enum SetPropertyResult {
16078 		success = 0, /// the property has been successfully set to the request value
16079 		notPermitted = -1, /// the property exists but it cannot be changed at this time
16080 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
16081 		noSuchProperty = -3, /// there is no property by that name
16082 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
16083 		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)
16084 	}
16085 
16086 	/++
16087 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
16088 
16089 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
16090 
16091 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
16092 		rarely need to use these building blocks directly.
16093 	+/
16094 	mixin template RegisterSetters() {
16095 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
16096 			switch(name) {
16097 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
16098 					case memberName:
16099 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
16100 							if(value != "true" && value != "false")
16101 								return SetPropertyResult.wrongFormat;
16102 							__traits(getMember, this, memberName) = value == "true" ? true : false;
16103 							return SetPropertyResult.success;
16104 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
16105 							import core.stdc.stdlib;
16106 							char[128] zero = 0;
16107 							if(buffer.length + 1 >= zero.length)
16108 								return SetPropertyResult.wrongFormat;
16109 							zero[0 .. buffer.length] = buffer[];
16110 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
16111 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
16112 							import core.stdc.stdlib;
16113 							char[128] zero = 0;
16114 							if(buffer.length + 1 >= zero.length)
16115 								return SetPropertyResult.wrongFormat;
16116 							zero[0 .. buffer.length] = buffer[];
16117 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
16118 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
16119 							__traits(getMember, this, memberName) = value.idup;
16120 						} else {
16121 							return SetPropertyResult.notImplemented;
16122 						}
16123 
16124 				}
16125 				default:
16126 					return super.setPropertyFromString(name, value, valueIsJson);
16127 			}
16128 		}
16129 	}
16130 
16131 	/++
16132 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
16133 
16134 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
16135 
16136 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
16137 		rarely need to use these building blocks directly.
16138 	+/
16139 	mixin template RegisterGetters() {
16140 		override void getPropertiesList(scope void delegate(string name) sink) const {
16141 			super.getPropertiesList(sink);
16142 
16143 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
16144 				sink(memberName);
16145 			}
16146 		}
16147 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
16148 			switch(name) {
16149 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
16150 					case memberName:
16151 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
16152 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
16153 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
16154 							import core.stdc.stdio;
16155 							char[32] buffer;
16156 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
16157 							sink(name, buffer[0 .. len], true);
16158 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
16159 							import core.stdc.stdio;
16160 							char[32] buffer;
16161 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
16162 							sink(name, buffer[0 .. len], true);
16163 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
16164 							sink(name, __traits(getMember, this, memberName), false);
16165 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
16166 						} else {
16167 							sink(name, null, true);
16168 						}
16169 
16170 					return;
16171 				}
16172 				default:
16173 					return super.getPropertyAsString(name, sink);
16174 			}
16175 		}
16176 	}
16177 }
16178 
16179 private struct Stack(T) {
16180 	this(int maxSize) {
16181 		internalLength = 0;
16182 		arr = initialBuffer[];
16183 	}
16184 
16185 	///.
16186 	void push(T t) {
16187 		if(internalLength >= arr.length) {
16188 			auto oldarr = arr;
16189 			if(arr.length < 4096)
16190 				arr = new T[arr.length * 2];
16191 			else
16192 				arr = new T[arr.length + 4096];
16193 			arr[0 .. oldarr.length] = oldarr[];
16194 		}
16195 
16196 		arr[internalLength] = t;
16197 		internalLength++;
16198 	}
16199 
16200 	///.
16201 	T pop() {
16202 		assert(internalLength);
16203 		internalLength--;
16204 		return arr[internalLength];
16205 	}
16206 
16207 	///.
16208 	T peek() {
16209 		assert(internalLength);
16210 		return arr[internalLength - 1];
16211 	}
16212 
16213 	///.
16214 	@property bool empty() {
16215 		return internalLength ? false : true;
16216 	}
16217 
16218 	///.
16219 	private T[] arr;
16220 	private size_t internalLength;
16221 	private T[64] initialBuffer;
16222 	// 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),
16223 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
16224 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
16225 }
16226 
16227 /// 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.
16228 private struct WidgetStream {
16229 
16230 	///.
16231 	@property Widget front() {
16232 		return current.widget;
16233 	}
16234 
16235 	/// Use Widget.tree instead.
16236 	this(Widget start) {
16237 		current.widget = start;
16238 		current.childPosition = -1;
16239 		isEmpty = false;
16240 		stack = typeof(stack)(0);
16241 	}
16242 
16243 	/*
16244 		Handle it
16245 		handle its children
16246 
16247 	*/
16248 
16249 	///.
16250 	void popFront() {
16251 	    more:
16252 	    	if(isEmpty) return;
16253 
16254 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
16255 
16256 		current.childPosition++;
16257 		if(current.childPosition >= current.widget.children.length) {
16258 			if(stack.empty())
16259 				isEmpty = true;
16260 			else {
16261 				current = stack.pop();
16262 				goto more;
16263 			}
16264 		} else {
16265 			stack.push(current);
16266 			current.widget = current.widget.children[current.childPosition];
16267 			current.childPosition = -1;
16268 		}
16269 	}
16270 
16271 	///.
16272 	@property bool empty() {
16273 		return isEmpty;
16274 	}
16275 
16276 	private:
16277 
16278 	struct Current {
16279 		Widget widget;
16280 		int childPosition;
16281 	}
16282 
16283 	Current current;
16284 
16285 	Stack!(Current) stack;
16286 
16287 	bool isEmpty;
16288 }
16289 
16290 
16291 /+
16292 
16293 	I could fix up the hierarchy kinda like this
16294 
16295 	class Widget {
16296 		Widget[] children() { return null; }
16297 	}
16298 	interface WidgetContainer {
16299 		Widget asWidget();
16300 		void addChild(Widget w);
16301 
16302 		// alias asWidget this; // but meh
16303 	}
16304 
16305 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
16306 
16307 	class Layout : Widget, WidgetContainer {}
16308 
16309 	class Window : WidgetContainer {}
16310 
16311 
16312 	All constructors that previously took Widgets should now take WidgetContainers instead
16313 
16314 
16315 
16316 	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".
16317 +/
16318 
16319 /+
16320 	LAYOUTS 2.0
16321 
16322 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
16323 
16324 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
16325 
16326 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
16327 
16328 	and even Paint can just use computedStyle...
16329 
16330 		background color
16331 		font
16332 		border color and style
16333 
16334 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
16335 		please note that many widgets and in some modes will completely ignore properties as they will.
16336 		they are just hints you set, not promises.
16337 
16338 
16339 
16340 
16341 
16342 	So generally the existing virtual functions are just the default for the class. But individual objects
16343 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
16344 +/
16345 
16346 /++
16347 	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.
16348 
16349 	History:
16350 		Added May 24, 2021.
16351 +/
16352 struct WidgetBackground {
16353 	/++
16354 		A background with the given solid color.
16355 	+/
16356 	this(Color color) {
16357 		this.color = color;
16358 	}
16359 
16360 	this(WidgetBackground bg) {
16361 		this = bg;
16362 	}
16363 
16364 	/++
16365 		Creates a widget from the string.
16366 
16367 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
16368 	+/
16369 	static WidgetBackground fromString(string s) {
16370 		return WidgetBackground(Color.fromString(s));
16371 	}
16372 
16373 	/++
16374 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
16375 
16376 		History:
16377 			Made `public` on December 18, 2022 (dub v10.10).
16378 	+/
16379 	Color color;
16380 }
16381 
16382 /++
16383 	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!)
16384 
16385 	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.
16386 
16387 	You should not inherit from this directly, but instead use [VisualTheme].
16388 
16389 	History:
16390 		Added May 8, 2021
16391 +/
16392 abstract class BaseVisualTheme {
16393 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
16394 	abstract void doPaint(Widget widget, WidgetPainter painter);
16395 
16396 	/+
16397 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
16398 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
16399 	+/
16400 
16401 	/++
16402 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
16403 		where the interpretation of the string varies for each property and may include things like measurement units.
16404 	+/
16405 	abstract string getPropertyString(Widget widget, string propertyName);
16406 
16407 	/++
16408 		Default background color of the window. Widgets also use this to simulate transparency.
16409 
16410 		Probably some shade of grey.
16411 	+/
16412 	abstract Color windowBackgroundColor();
16413 	abstract Color widgetBackgroundColor();
16414 	abstract Color foregroundColor();
16415 	abstract Color lightAccentColor();
16416 	abstract Color darkAccentColor();
16417 
16418 	/++
16419 		Colors used to indicate active selections in lists and text boxes, etc.
16420 	+/
16421 	abstract Color selectionForegroundColor();
16422 	/// ditto
16423 	abstract Color selectionBackgroundColor();
16424 
16425 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
16426 
16427 	/++
16428 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
16429 	+/
16430 	abstract OperatingSystemFont defaultFont(int dpi);
16431 
16432 	private OperatingSystemFont[int] defaultFontCache_;
16433 	private OperatingSystemFont defaultFontCached(int dpi) {
16434 		if(dpi !in defaultFontCache_) {
16435 			// FIXME: set this to false if X disconnect or if visual theme changes
16436 			defaultFontCache_[dpi] = defaultFont(dpi);
16437 		}
16438 		return defaultFontCache_[dpi];
16439 	}
16440 }
16441 
16442 /+
16443 	A widget should have:
16444 		classList
16445 		dataset
16446 		attributes
16447 		computedStyles
16448 		state (persistent)
16449 		dynamic state (focused, hover, etc)
16450 +/
16451 
16452 // visualTheme.computedStyle(this).paddingLeft
16453 
16454 
16455 /++
16456 	This is your entry point to create your own visual theme for custom widgets.
16457 
16458 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
16459 
16460 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
16461 +/
16462 abstract class VisualTheme(CRTP) : BaseVisualTheme {
16463 	override string getPropertyString(Widget widget, string propertyName) {
16464 		return null;
16465 	}
16466 
16467 	/+
16468 		mixin StyleOverride!Widget
16469 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
16470 		w.useStyleProperties(dg);
16471 	}
16472 	+/
16473 
16474 	final override void doPaint(Widget widget, WidgetPainter painter) {
16475 		auto derived = cast(CRTP) cast(void*) this;
16476 
16477 		scope void delegate(Widget, WidgetPainter) bestMatch;
16478 		int bestMatchScore;
16479 
16480 		static if(__traits(hasMember, CRTP, "paint"))
16481 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
16482 			static if(is(typeof(overload) Params == __parameters)) {
16483 				static assert(Params.length == 2);
16484 				static assert(is(Params[0] : Widget));
16485 				static assert(is(Params[1] == WidgetPainter));
16486 				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);
16487 
16488 				alias type = Params[0];
16489 				if(cast(type) widget) {
16490 					auto score = baseClassCount!type;
16491 
16492 					if(score > bestMatchScore) {
16493 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
16494 						bestMatchScore = score;
16495 					}
16496 				}
16497 			} else static assert(0, "paint should be a method.");
16498 		}
16499 
16500 		if(bestMatch)
16501 			bestMatch(widget, painter);
16502 		else
16503 			widget.paint(painter);
16504 	}
16505 
16506 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
16507 
16508 	// 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
16509 	// mixin Beautiful95Theme;
16510 	mixin DefaultLightTheme;
16511 
16512 	private static struct Cached {
16513 		// i prolly want to do this
16514 	}
16515 }
16516 
16517 /// ditto
16518 mixin template Beautiful95Theme() {
16519 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
16520 	override Color widgetBackgroundColor() { return Color.white; }
16521 	override Color foregroundColor() { return Color.black; }
16522 	override Color darkAccentColor() { return Color(172, 172, 172); }
16523 	override Color lightAccentColor() { return Color(223, 223, 223); }
16524 	override Color selectionForegroundColor() { return Color.white; }
16525 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
16526 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
16527 }
16528 
16529 /// ditto
16530 mixin template DefaultLightTheme() {
16531 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
16532 	override Color widgetBackgroundColor() { return Color.white; }
16533 	override Color foregroundColor() { return Color.black; }
16534 	override Color darkAccentColor() { return Color(172, 172, 172); }
16535 	override Color lightAccentColor() { return Color(223, 223, 223); }
16536 	override Color selectionForegroundColor() { return Color.white; }
16537 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
16538 	override OperatingSystemFont defaultFont(int dpi) {
16539 		version(Windows)
16540 			return new OperatingSystemFont("Segoe UI");
16541 		else static if(UsingSimpledisplayCocoa) {
16542 			return (new OperatingSystemFont()).loadDefault;
16543 		} else {
16544 			// FIXME: undo xft's scaling so we don't end up double scaled
16545 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
16546 		}
16547 	}
16548 }
16549 
16550 /// ditto
16551 mixin template DefaultDarkTheme() {
16552 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
16553 	override Color widgetBackgroundColor() { return Color.black; }
16554 	override Color foregroundColor() { return Color.white; }
16555 	override Color darkAccentColor() { return Color(20, 20, 20); }
16556 	override Color lightAccentColor() { return Color(80, 80, 80); }
16557 	override Color selectionForegroundColor() { return Color.white; }
16558 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
16559 	override OperatingSystemFont defaultFont(int dpi) {
16560 		version(Windows)
16561 			return new OperatingSystemFont("Segoe UI", 12);
16562 		else static if(UsingSimpledisplayCocoa) {
16563 			return (new OperatingSystemFont()).loadDefault;
16564 		} else {
16565 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
16566 		}
16567 	}
16568 }
16569 
16570 /// ditto
16571 alias DefaultTheme = DefaultLightTheme;
16572 
16573 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
16574 	/+
16575 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
16576 	Color windowBackgroundColor() { return Color(242, 242, 242); }
16577 	Color darkAccentColor() { return windowBackgroundColor; }
16578 	Color lightAccentColor() { return windowBackgroundColor; }
16579 	+/
16580 }
16581 
16582 /++
16583 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
16584 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
16585 
16586 	History:
16587 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16588 +/
16589 class StateChanged(alias field) : Event {
16590 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
16591 	override bool cancelable() const { return false; }
16592 	this(Widget target, typeof(field) newValue) {
16593 		this.newValue = newValue;
16594 		super(EventString, target);
16595 	}
16596 
16597 	typeof(field) newValue;
16598 }
16599 
16600 /++
16601 	Convenience function to add a `triggered` event listener.
16602 
16603 	Its implementation is simply `w.addEventListener("triggered", dg);`
16604 
16605 	History:
16606 		Added November 27, 2021 (dub v10.4)
16607 +/
16608 void addWhenTriggered(Widget w, void delegate() dg) {
16609 	w.addEventListener("triggered", dg);
16610 }
16611 
16612 /++
16613 	Observable varables can be added to widgets and when they are changed, it fires
16614 	off a [StateChanged] event so you can react to it.
16615 
16616 	It is implemented as a getter and setter property, along with another helper you
16617 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
16618 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
16619 	example.
16620 
16621 	History:
16622 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
16623 +/
16624 mixin template Observable(T, string name) {
16625 	private T backing;
16626 
16627 	mixin(q{
16628 		void } ~ name ~ q{_changed (void delegate(T) dg) {
16629 			this.addEventListener((StateChanged!this_thing ev) {
16630 				dg(ev.newValue);
16631 			});
16632 		}
16633 
16634 		@property T } ~ name ~ q{ () {
16635 			return backing;
16636 		}
16637 
16638 		@property void } ~ name ~ q{ (T t) {
16639 			backing = t;
16640 			auto event = new StateChanged!this_thing(this, t);
16641 			event.dispatch();
16642 		}
16643 	});
16644 
16645 	mixin("private alias this_thing = " ~ name ~ ";");
16646 }
16647 
16648 
16649 private bool startsWith(string test, string thing) {
16650 	if(test.length < thing.length)
16651 		return false;
16652 	return test[0 .. thing.length] == thing;
16653 }
16654 
16655 private bool endsWith(string test, string thing) {
16656 	if(test.length < thing.length)
16657 		return false;
16658 	return test[$ - thing.length .. $] == thing;
16659 }
16660 
16661 /++
16662 	Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon`
16663 
16664 	Note they can NOT have accelerators or toolbars; those annotations will be ignored.
16665 
16666 	Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time.
16667 
16668 	See_Also:
16669 		[Widget.setMenuAndToolbarFromAnnotatedCode]
16670 +/
16671 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) {
16672 	return createContextMenuFromAnnotatedCode(w, w);
16673 }
16674 
16675 /// ditto
16676 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) {
16677 	return createContextMenuFromAnnotatedCode_internal(w, t);
16678 }
16679 /// ditto
16680 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) {
16681 	return createContextMenuFromAnnotatedCode_internal(w, t);
16682 }
16683 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) {
16684 	Menu ret = new Menu("", w);
16685 
16686 	foreach(memberName; __traits(derivedMembers, T)) {
16687 		static if(memberName != "this")
16688 		static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
16689 			.menu menu;
16690 			bool separator;
16691 			.hotkey hotkey;
16692 			.icon icon;
16693 			string label;
16694 			string tip;
16695 			foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
16696 				static if(is(typeof(attr) == .menu))
16697 					menu = attr;
16698 				else static if(is(attr == .separator))
16699 					separator = true;
16700 				else static if(is(typeof(attr) == .hotkey))
16701 					hotkey = attr;
16702 				else static if(is(typeof(attr) == .icon))
16703 					icon = attr;
16704 				else static if(is(typeof(attr) == .label))
16705 					label = attr.label;
16706 				else static if(is(typeof(attr) == .tip))
16707 					tip = attr.tip;
16708 			}
16709 
16710 			if(menu is .menu.init) {
16711 				ushort correctIcon = icon.id; // FIXME
16712 				if(label.length == 0)
16713 					label = memberName.toMenuLabel;
16714 
16715 				auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName));
16716 
16717 				auto action = new Action(label, correctIcon, handler);
16718 
16719 				if(separator)
16720 					ret.addSeparator();
16721 					ret.addItem(new MenuItem(action));
16722 			}
16723 		}
16724 	}
16725 
16726 	return ret;
16727 }
16728 
16729 // still do layout delegation
16730 // and... split off Window from Widget.
16731 
16732 version(minigui_screenshots)
16733 struct Screenshot {
16734 	string name;
16735 }
16736 
16737 version(minigui_screenshots)
16738 static if(__VERSION__ > 2092)
16739 mixin(q{
16740 shared static this() {
16741 	import core.runtime;
16742 
16743 	static UnitTestResult screenshotMagic() {
16744 		string name;
16745 
16746 		import arsd.png;
16747 
16748 		auto results = new Window();
16749 		auto button = new Button("do it", results);
16750 
16751 		Window.newWindowCreated = delegate(Window w) {
16752 			Timer timer;
16753 			timer = new Timer(250, {
16754 				auto img = w.win.takeScreenshot();
16755 				timer.destroy();
16756 
16757 				version(Windows)
16758 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
16759 				else
16760 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
16761 
16762 				w.close();
16763 			});
16764 		};
16765 
16766 		button.addWhenTriggered( {
16767 
16768 		foreach(test; __traits(getUnitTests, mixin(__MODULE__))) {
16769 			name = null;
16770 			static foreach(attr; __traits(getAttributes, test)) {
16771 				static if(is(typeof(attr) == Screenshot))
16772 					name = attr.name;
16773 			}
16774 			if(name.length) {
16775 				test();
16776 			}
16777 		}
16778 
16779 		});
16780 
16781 		results.loop();
16782 
16783 		return UnitTestResult(0, 0, false, false);
16784 	}
16785 
16786 
16787 	Runtime.extendedModuleUnitTester = &screenshotMagic;
16788 }
16789 });
16790 version(minigui_screenshots) {
16791 	version(unittest)
16792 		void main() {}
16793 	else static assert(0, "dont forget the -unittest flag to dmd");
16794 }
16795 
16796 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
16797 // FIXME: make multiple accelerators disambiguate based ona rgs
16798 // FIXME: MainWindow ctor should have same arg order as Window
16799 // FIXME: mainwindow ctor w/ client area size instead of total size.
16800 // 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.
16801 // FIXME: tri-state checkbox
16802 // FIXME: subordinate controls grouping...