1 /+
2 	BreakpointSplitter
3 		- if not all widgets fit, it collapses to tabs
4 		- if they do, you get a splitter
5 		- you set priority to display things first and optional breakpoint (otherwise it uses flex basis and min width)
6 +/
7 
8 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
9 
10 // 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
11 
12 // me@arsd:~/.kde/share/config$ vim kdeglobals
13 
14 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too.
15 
16 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/
17 
18 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever.
19 
20 // responsive minigui, menu search, and file open with a preview hook on the side.
21 
22 // FIXME: add menu checkbox and menu icon eventually
23 
24 // FOXME: look at Windows rebar control too
25 
26 /*
27 
28 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
29 
30 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it...
31 */
32 
33 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places
34 
35 // FIXME: text label must be copyable to the clipboard, at least as a full chunk.
36 
37 // FIXME: opt-in file picker widget with image support
38 
39 // FIXME: number widget
40 
41 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm
42 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles
43 
44 // osx style menu search.
45 
46 // would be cool for a scroll bar to have marking capabilities
47 // kinda like vim's marks just on clicks etc and visual representation
48 // generically. may be cool to add an up arrow to the bottom too
49 //
50 // leave a shadow of where you last were for going back easily
51 
52 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various
53 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for
54 // the window.
55 
56 // so what about context menus?
57 
58 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
59 
60 // FIXME: make the scroll thing go to bottom when the content changes.
61 
62 // 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
63 
64 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
65 
66 
67 // FIXME: add a command search thingy built in and implement tip.
68 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!
69 
70 // On Windows:
71 // FIXME: various labels look broken in high contrast mode
72 // FIXME: changing themes while the program is upen doesn't trigger a redraw
73 
74 // add note about manifest to documentation. also icons.
75 
76 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar
77 // FIXME: clear the corner of scrollbars if they pop up
78 
79 // minigui needs to have a stdout redirection for gui mode on windows writeln
80 
81 // I kinda wanna do state reacting. sort of. idk tho
82 
83 // need a viewer widget that works like a web page - arrows scroll down consistently
84 
85 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside.
86 
87 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two.
88 // and help info about menu items.
89 // and search in menus?
90 
91 // FIXME: a scroll area event signaling when a thing comes into view might be good
92 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must
93 
94 // FIXME: unify Windows style line endings
95 
96 /*
97 	TODO:
98 
99 	pie menu
100 
101 	class Form with submit behavior -- see AutomaticDialog
102 
103 	disabled widgets and menu items
104 
105 	event cleanup
106 	tooltips.
107 	api improvements
108 
109 	margins are kinda broken, they don't collapse like they should. at least.
110 
111 	a table form btw would be a horizontal layout of vertical layouts holding each column
112 	that would give the same width things
113 */
114 
115 /*
116 
117 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
118 */
119 
120 /++
121 	minigui is a smallish GUI widget library, aiming to be on par with at least
122 	HTML4 forms and a few other expected gui components. It uses native controls
123 	on Windows and does its own thing on Linux (Mac is not currently supported but
124 	I'm slowly working on it).
125 
126 
127 	$(H3 Conceptual Overviews)
128 
129 	A gui application is made out of widgets laid out in windows that display information and respond to events from the user. They also typically have actions available in menus, and you might also want to customize the appearance. How do we do these things with minigui? Let's break it down into several categories.
130 
131 	$(H4 Code structure)
132 
133 	You will typically want to create the ui, prepare event handlers, then run an event loop. The event loop drives the program, calling your methods to respond to user activity.
134 
135 	---
136 	import arsd.minigui;
137 
138 	void main() {
139 		// first, create a window, the (optional) string here is its title
140 		auto window = new MainWindow("Hello, World!");
141 
142 		// lay out some widgets inside the window to create the ui
143 		auto name = new LabeledLineEdit("What is your name?", window);
144 		auto button = new Button("Say Hello", window);
145 
146 		// prepare event handlers
147 		button.addEventListener(EventType.triggered, () {
148 			window.messageBox("Hello, " ~ name.content ~ "!");
149 		});
150 
151 		// show the window and run the event loop until this window is closed
152 		window.loop();
153 	}
154 	---
155 
156 	To compile, run `opend hello.d`, then run the generated `hello` program.
157 
158 	While the specifics will change, nearly all minigui applications will roughly follow this pattern.
159 
160 	$(TIP
161 		There are two other ways to run event loops: `arsd.simpledisplay.EventLoop.get.run();` and `arsd.core.getThisThreadEventLoop().run();`. They all call the same underlying functions, but have different exit conditions - the `EventLoop.get.run()` keeps running until all top-level windows are closed, and `getThisThreadEventLoop().run` keeps running until all "tasks are resolved"; it is more abstract, supporting more than just windows.
162 
163 		You may call this if you don't have a single main window.
164 
165 		Even a basic minigui window can benefit from these if you don't have a single main window:
166 
167 		---
168 		import arsd.minigui;
169 
170 		void main() {
171 			// create a struct to hold gathered info
172 			struct Hello { string name; }
173 			// let minigui create a dialog box to get that
174 			// info from the user. If you have a main window,
175 			// you'd pass that here, but it is not required
176 			dialog((Hello info) {
177 				// inline handler of the "OK" button
178 				messageBox("Hello, " ~ info.name);
179 			});
180 
181 			// since there is no main window to loop on,
182 			// we instead call the event loop singleton ourselves
183 			EventLoop.get.run;
184 		}
185 		---
186 
187 		This is also useful when your programs lives as a notification area (aka systray) icon instead of as a window. But let's not get too far ahead of ourselves!
188 	)
189 
190 	$(H4 How to lay out widgets)
191 
192 	To better understand the details of layout algorithms and see more available included classes, see [Layout].
193 
194 	$(H5 Default layouts)
195 
196 	minigui windows default to a flexible vertical layout, where widgets are added, from top to bottom on the window, in the same order of you creating them, then they are sized according to layout hints on the widget itself to fill the available space. This gives a reasonably usable setup but you'll probably want to customize it.
197 
198 	$(TIP
199 		minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off.
200 	)
201 
202 	Generally speaking, there are two ways to customize layouts: either subclass the widget and change its hints, or wrap it in another layout widget. You can also create your own layout classes and do it all yourself, but that's fairly complicated. Wrapping existing widgets in other layout widgets is usually the easiest way to make things work.
203 
204 	$(NOTE
205 		minigui widgets are not supposed to overlap, but can contain children, and are always rectangular. Children are laid out as rectangles inside the parent's rectangular area.
206 	)
207 
208 	For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]:
209 
210 	---
211 	import arsd.minigui;
212 	void main() {
213 		auto window = new MainWindow();
214 
215 		// make the layout a child of our window
216 		auto hl = new HorizontalLayout(window);
217 
218 		// then make the widgets children of the layout
219 		auto leftButton = new Button("Left", hl);
220 		auto rightButton = new Button("Right", hl);
221 
222 		window.loop();
223 	}
224 	---
225 
226 	A [HorizontalLayout] works just like the default [VerticalLayout], except in the other direction. These two buttons will take up all the available vertical space, then split available horizontal space equally.
227 
228 	$(H5 Nesting layouts)
229 
230 	Nesting layouts lets you carve up the rectangle in different ways.
231 
232 	$(EMBED_UNITTEST layout-example)
233 
234 	$(H5 Special layouts)
235 
236 	[TabWidget] can show pages of layouts as tabs.
237 
238 	See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead.
239 
240 	$(H5 Other common layout classes)
241 
242 	[HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout]
243 
244 	$(H4 How to respond to widget events)
245 
246 	To better understanding the underlying event system, see [Event].
247 
248 	Each widget emits its own events, which propagate up through their parents until they reach their top-level window.
249 
250 	$(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.)
251 
252 	We started this series with a [MainWindow], but only added widgets to it. MainWindows also support menus and toolbars with various keyboard shortcuts. You can construct these menus by constructing classes and calling methods, but minigui also lets you just write functions in a command object and it does the rest!
253 
254 	See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example.
255 
256 	Note that toggleable menu or toolbar items are not yet implemented, but on the todolist. Submenus and disabled items are also not supported at this time and not currently on the work list (but if you need it, let me know and MAYBE we can work something out. Emphasis on $(I maybe)).
257 
258 	$(TIP
259 		The automatic dialog box logic is also available for you to invoke on demand with [dialog] and the data setting logic can be used with a child widget inside an existing window [addDataControllerWidget], which also has annotation-based layout capabilities.
260 	)
261 
262 	All windows also have titles. You can change this at any time with the `window.title = "string";` property.
263 
264 	Windows also have icons, which can be set with the `window.icon` property. It takes a [arsd.color.MemoryImage] object, which is an in-memory bitmap. [arsd.image] can load common file formats into these objects, or you can make one yourself. The default icon on Windows is the icon of your exe, which you can set through a resource file. (FIXME: explain how to do this easily.)
265 
266 	The `MainWindow` also provides a status bar across the bottom. These aren't so common in new applications, but I love them - on my own computer, I even have a global status bar for my whole desktop! I suggest you use it: a status bar is a consistent place to put information and notifications that will never overlap other content.
267 
268 	A status bar has parts, and the parts have content. The first part's content is assumed to change frequently; the default mouse over event will set it to [Widget.statusTip], a public `string` you can assign to any widget you want at any time.
269 
270 	Other parts can be added by you and are under your control. You add them with:
271 
272 	---
273 	window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units);
274 	---
275 
276 	The size can be in a variety of units and what you get with mixes can get complicated. The rule is: explicit pixel sizes are used first. Then, proportional sizes are applied to the remaining space. Then, finally, if there is any space left, any items without an explicit size split them equally.
277 
278 	You may prefer to set them all at once, with:
279 
280 	---
281 	window.statusBar.parts.setSizes(1, 1, 1);
282 	---
283 
284 	This makes a three-part status bar, each with the same size - they all take the same proportion of the total size. Negative numbers here will use auto-scaled pixels.
285 
286 	You should call this right after creating your `MainWindow` as part of your setup code.
287 
288 	Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";`
289 
290 	$(NOTE
291 		I'm thinking about making the other parts do other things by default too, but if I do change it, I'll try not to break any explicitly set things you do anyway.
292 	)
293 
294 	If you really don't want a status bar on your main window, you can remove it with `window.statusBar = null;` Make sure you don't try to use it again, or your program will likely crash!
295 
296 	Status bars, at this time, cannot hold non-text content, but I do want to change that. They also cannot have event listeners at this time, but again, that is likely to change. I have something in mind where they can hold clickable messages with a history and maybe icons, but haven't implemented any of that yet. Right now, they're just a (still very useful!) display area.
297 
298 	$(H4 How to do custom styles)
299 
300 	Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s.
301 
302 	$(WARNING
303 		These don't apply to non-custom widgets! They will use the operating system's native theme unless the documentation for that specific class says otherwise.
304 
305 		At this time, custom widgets gain capability in styling, but lose capability in terms of keeping all the right integrated details of the user experience and availability to accessibility and other automation tools. Evaluate if the benefit is worth the costs before making your decision.
306 
307 		I'd like to erase more and more of these gaps, but no promises as to when - or even if - that will ever actually happen.
308 	)
309 
310 	See [Widget.Style] for more information.
311 
312 	$(H4 Selection of categorized widgets)
313 
314 	$(LIST
315 		* Buttons: [Button]
316 		* Text display widgets: [TextLabel], [TextDisplay]
317 		* Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit]
318 		* Selecting multiple on/off options: [Checkbox]
319 		* Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection]
320 		* Getting rough numeric input: [HorizontalSlider], [VerticalSlider]
321 		* Displaying data: [ImageBox], [ProgressBar], [TableView]
322 		* Showing a list of editable items: [GenericListViewWidget]
323 		* Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget]
324 	)
325 
326 	And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons].
327 
328 	If none of these do what you need, you'll want to write your own. More on that in the following section.
329 
330 	$(H4 custom widgets - how to write your own)
331 
332 	See [Widget].
333 
334 	If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too!
335 
336 		If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required.
337 
338 		See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points.
339 
340 		You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada.
341 
342 		You might make a compound widget out of other widgets. [Widget.encapsulatedChildren] can help hide this from the outside world (though is not necessary and might hurt some debugging!)
343 
344 		$(TIP
345 			Compile your application with the `-debug` switch and press F12 in your window to open a web-browser-inspired debug window. It sucks right now and doesn't do a lot, but is sometimes better than nothing.
346 		)
347 
348 	$(H5 Timers and animations)
349 
350 	The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler.
351 
352 	I generally don't like animations in my programs, so it hasn't been a priority for me to do more than this. I also hate uis that move outside of explicit user action, so minigui kinda supports this but I'd rather you didn't. I kinda wanna do something like `requestAnimationFrame` or something but haven't yet so it is just the `Timer` class.
353 
354 	$(H5 Clipboard integrations, drag and drop)
355 
356 	GUI application users tend to expect integration with their system, so clipboard support is basically a must, and drag and drop is nice to offer too. The functions for these are provided in [arsd.simpledisplay], which is public imported from minigui, and thus available to you here too.
357 
358 	I'd like to think of some better abstractions to make this more automagic, but you must do it yourself when implementing your custom widgets right now.
359 
360 	See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay.
361 
362 	$(H5 Context menus)
363 
364 	Override [Widget.contextMenu] in your subclass.
365 
366 	$(H4 Coming later)
367 
368 	Among the unfinished features: unified selections, translateable strings, external integrations.
369 
370 	$(H2 Running minigui programs)
371 
372 	Note the environment variable ARSD_SCALING_FACTOR on Linux can set multi-monitor scaling factors. I should also read it from a root window property so it easier to do with migrations... maybe a default theme selector from there too.
373 
374 	$(H2 Building minigui programs)
375 
376 	minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and
377 	[arsd.textlayouter], on which it is built. simpledisplay provides the low-level
378 	interfaces and minigui builds the concept of widgets inside the windows on top of it.
379 
380 	Its #1 goal is to be useful without being large and complicated like GTK and Qt.
381 	It isn't hugely concerned with appearance - on Windows, it just uses the native
382 	controls and native theme, and on Linux, it keeps it simple and I may change that
383 	at any time, though after May 2021, you can customize some things with css-inspired
384 	[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
385 	you can use the custom implementation there too, but... you shouldn't.)
386 
387 	The event model is similar to what you use in the browser with Javascript and the
388 	layout engine tries to automatically fit things in, similar to a css flexbox.
389 
390 	FOR BEST RESULTS: be sure to link with the appropriate subsystem command
391 	`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
392 	of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
393 
394 	Otherwise you'll get a console and possibly other visual bugs. But if you do use
395 	the subsystem:windows, note that Phobos' writeln will crash the program!
396 
397 	HTML_To_Classes:
398 	$(SMALL_TABLE
399 		HTML Code | Minigui Class
400 
401 		`<input type="text">` | [LineEdit]
402 		`<input type="password">` | [PasswordEdit]
403 		`<textarea>` | [TextEdit]
404 		`<select>` | [DropDownSelection]
405 		`<input type="checkbox">` | [Checkbox]
406 		`<input type="radio">` | [Radiobox]
407 		`<button>` | [Button]
408 	)
409 
410 
411 	Stretchiness:
412 		The default is 4. You can use larger numbers for things that should
413 		consume a lot of space, and lower numbers for ones that are better at
414 		smaller sizes.
415 
416 	Overlapped_input:
417 		COMING EVENTUALLY:
418 		minigui will include a little bit of I/O functionality that just works
419 		with the event loop. If you want to get fancy, I suggest spinning up
420 		another thread and posting events back and forth.
421 
422 	$(H2 Add ons)
423 		See the `minigui_addons` directory in the arsd repo for some add on widgets
424 		you can import separately too.
425 
426 	$(H3 XML definitions)
427 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
428 
429 	$(H3 Scriptability)
430 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
431 		in this documentation, it means you can call it from the script language.
432 
433 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
434 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
435 
436 		---
437 		import arsd.minigui_xml;
438 		import arsd.script;
439 
440 		var globals = var.emptyObject;
441 		globals.makeWidgetFromString = &makeWidgetFromString;
442 
443 		// this now works
444 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
445 		---
446 
447 		More to come.
448 
449 	My_UI_Guidelines:
450 		Note that the Linux custom widgets generally aim to be efficient on remote X network connections.
451 
452 		In a perfect world, you'd achieve all the following goals:
453 
454 		$(LIST
455 			* All operations are present in the menu
456 			* The operations the user wants at the moment are right where they want them
457 			* All operations can be scripted
458 			* The UI does not move any elements without explicit user action
459 			* All numbers can be seen and typed in if wanted, even if the ui usually hides them
460 		)
461 
462 	$(H2 Future Directions)
463 
464 	I want to do some newer ideas that might not be easy to keep working fully on Windows, like adding a menu search feature and scrollbar custom marks and typing in numbers. I might make them a default part of the widget with custom, and let you provide them through a menu or something elsewhere.
465 
466 	History:
467 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
468 
469 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
470 		tag this as version 2.0.
471 
472 		Among the changes:
473 		$(LIST
474 			* 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.
475 
476 			See [Event] for details.
477 
478 			* 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.
479 
480 			See [DoubleClickEvent] for details.
481 
482 			* 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.
483 
484 			See [Widget.Style] for details.
485 
486 			* 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.
487 
488 			* 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.
489 
490 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
491 
492 			* 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.
493 
494 			* Various non-breaking additions.
495 		)
496 +/
497 module arsd.minigui;
498 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
499 
500 /++
501 	This hello world sample will have an oversized button, but that's ok, you see your first window!
502 +/
503 version(Demo)
504 unittest {
505 	import arsd.minigui;
506 
507 	void main() {
508 		auto window = new MainWindow();
509 
510 		// note the parent widget is almost always passed as the last argument to a constructor
511 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
512 		auto button = new Button("Close", window);
513 		button.addWhenTriggered({
514 			window.close();
515 		});
516 
517 		window.loop();
518 	}
519 
520 	main(); // exclude from docs
521 }
522 
523 /++
524 	$(ID layout-example)
525 
526 	This example shows one way you can partition your window into a header
527 	and sidebar. Here, the header and sidebar have a fixed width, while the
528 	rest of the content sizes with the window.
529 
530 	It might be a new way of thinking about window layout to do things this
531 	way - perhaps [GridLayout] more matches your style of thought - but the
532 	concept here is to partition the window into sub-boxes with a particular
533 	size, then partition those boxes into further boxes.
534 
535 	$(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.)
536 
537 	So to make the header, start with a child layout that has a max height.
538 	It will use that space from the top, then the remaining children will
539 	split the remaining area, meaning you can think of is as just being another
540 	box you can split again. Keep splitting until you have the look you desire.
541 +/
542 // https://github.com/adamdruppe/arsd/issues/310
543 version(minigui_screenshots)
544 @Screenshot("layout")
545 unittest {
546 	import arsd.minigui;
547 
548 	// This helper class is just to help make the layout boxes visible.
549 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
550 	class ColorWidget : Widget {
551 		this(Color color, Widget parent) {
552 			this.color = color;
553 			super(parent);
554 		}
555 		Color color;
556 		class Style : Widget.Style {
557 			override WidgetBackground background() { return WidgetBackground(color); }
558 		}
559 		mixin OverrideStyle!Style;
560 	}
561 
562 	void main() {
563 		auto window = new Window;
564 
565 		// the key is to give it a max height. This is one way to do it:
566 		auto header = new class HorizontalLayout {
567 			this() { super(window); }
568 			override int maxHeight() { return 50; }
569 		};
570 		// this next line is a shortcut way of doing it too, but it only works
571 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
572 		// is good to know how to make a new class like above anyway.
573 		// auto header = new HorizontalLayout(50, window);
574 
575 		auto bar = new HorizontalLayout(window);
576 
577 		// or since this is so common, VerticalLayout and HorizontalLayout both
578 		// can just take an argument in their constructor for max width/height respectively
579 
580 		// (could have tone this above too, but I wanted to demo both techniques)
581 		auto left = new VerticalLayout(100, bar);
582 
583 		// and this is the main section's container. A plain Widget instance is good enough here.
584 		auto container = new Widget(bar);
585 
586 		// and these just add color to the containers we made above for the screenshot.
587 		// in a real application, you can just add your actual controls instead of these.
588 		auto headerColorBox = new ColorWidget(Color.teal, header);
589 		auto leftColorBox = new ColorWidget(Color.green, left);
590 		auto rightColorBox = new ColorWidget(Color.purple, container);
591 
592 		window.loop();
593 	}
594 
595 	main(); // exclude from docs
596 }
597 
598 
599 import arsd.core;
600 import arsd.textlayouter;
601 
602 alias Timer = arsd.simpledisplay.Timer;
603 public import arsd.simpledisplay;
604 /++
605 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
606 
607 	History:
608 		Was private until May 15, 2021.
609 +/
610 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
611 
612 version(Windows) {
613 	import core.sys.windows.winnls;
614 	import core.sys.windows.windef;
615 	import core.sys.windows.basetyps;
616 	import core.sys.windows.winbase;
617 	import core.sys.windows.winuser;
618 	import core.sys.windows.wingdi;
619 	static import gdi = core.sys.windows.wingdi;
620 }
621 
622 version(Windows) {
623 	version(minigui_manifest) {} else version=minigui_no_manifest;
624 
625 	version(minigui_no_manifest) {} else
626 	static if(__VERSION__ >= 2_083)
627 	version(CRuntime_Microsoft) { // FIXME: mingw?
628 		// assume we want commctrl6 whenever possible since there's really no reason not to
629 		// and this avoids some of the manifest hassle
630 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
631 	}
632 }
633 
634 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
635 private bool lastDefaultPrevented;
636 
637 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
638 alias scriptable = arsd_jsvar_compatible;
639 
640 version(Windows) {
641 	// use native widgets when available unless specifically asked otherwise
642 	version(custom_widgets) {
643 		enum bool UsingCustomWidgets = true;
644 		enum bool UsingWin32Widgets = false;
645 	} else {
646 		version = win32_widgets;
647 		enum bool UsingCustomWidgets = false;
648 		enum bool UsingWin32Widgets = true;
649 	}
650 	// and native theming when needed
651 	//version = win32_theming;
652 } else {
653 	enum bool UsingCustomWidgets = true;
654 	enum bool UsingWin32Widgets = false;
655 	version=custom_widgets;
656 }
657 
658 
659 
660 /*
661 
662 	The main goals of minigui.d are to:
663 		1) Provide basic widgets that just work in a lightweight lib.
664 		   I basically want things comparable to a plain HTML form,
665 		   plus the easy and obvious things you expect from Windows
666 		   apps like a menu.
667 		2) Use native things when possible for best functionality with
668 		   least library weight.
669 		3) Give building blocks to provide easy extension for your
670 		   custom widgets, or hooking into additional native widgets
671 		   I didn't wrap.
672 		4) Provide interfaces for easy interaction between third
673 		   party minigui extensions. (event model, perhaps
674 		   signals/slots, drop-in ease of use bits.)
675 		5) Zero non-system dependencies, including Phobos as much as
676 		   I reasonably can. It must only import arsd.color and
677 		   my simpledisplay.d. If you need more, it will have to be
678 		   an extension module.
679 		6) An easy layout system that generally works.
680 
681 	A stretch goal is to make it easy to make gui forms with code,
682 	some kind of resource file (xml?) and even a wysiwyg designer.
683 
684 	Another stretch goal is to make it easy to hook data into the gui,
685 	including from reflection. So like auto-generate a form from a
686 	function signature or struct definition, or show a list from an
687 	array that automatically updates as the array is changed. Then,
688 	your program focuses on the data more than the gui interaction.
689 
690 
691 
692 	STILL NEEDED:
693 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
694 		* slider
695 		* listbox
696 		* spinner
697 		* label?
698 		* rich text
699 */
700 
701 
702 /+
703 	enum LayoutMethods {
704 		 verticalFlex,
705 		 horizontalFlex,
706 		 inlineBlock, // left to right, no stretch, goes to next line as needed
707 		 static, // just set to x, y
708 		 verticalNoStretch, // browser style default
709 
710 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
711 
712 		 grid, // magic
713 	}
714 +/
715 
716 /++
717 	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.
718 
719 
720 	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.
721 
722 	---
723 	class MinimalWidget : Widget {
724 		this(Widget parent) {
725 			super(parent);
726 		}
727 	}
728 	---
729 
730 	$(SIDEBAR
731 		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.
732 	)
733 
734 	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.
735 
736 	Among the things you'll most likely want to change in your custom widget:
737 
738 	$(LIST
739 		* 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.)
740 
741 		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.
742 
743 		Do this $(I after) calling the `super` constructor.
744 
745 		* 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.
746 
747 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
748 
749 		* 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.
750 
751 		* 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.
752 	)
753 
754 	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.
755 
756 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
757 
758 	Your own custom-drawn and native system controls can exist side-by-side.
759 
760 	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.
761 +/
762 class Widget : ReflectableProperties {
763 
764 	private bool willDraw() {
765 		return true;
766 	}
767 
768 	/+
769 	/++
770 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
771 
772 		History:
773 			Added September 15, 2021
774 			implemented.... ???
775 	+/
776 	void prepareReflection(this This)() {
777 
778 	}
779 	+/
780 
781 	private bool _enabled = true;
782 
783 	/++
784 		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.
785 
786 		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.
787 
788 		History:
789 			Added November 23, 2021 (dub v10.4)
790 
791 			Warning: the specific behavior of disabling with parents may change in the future.
792 		Bugs:
793 			Currently only implemented for widgets backed by native Windows controls.
794 
795 		See_Also: [disabledReason], [disabledBy]
796 	+/
797 	@property bool enabled() {
798 		return disabledBy() is null;
799 	}
800 
801 	/// ditto
802 	@property void enabled(bool yes) {
803 		_enabled = yes;
804 		version(win32_widgets) {
805 			if(hwnd)
806 				EnableWindow(hwnd, yes);
807 		}
808 		setDynamicState(DynamicState.disabled, yes);
809 	}
810 
811 	private string disabledReason_;
812 
813 	/++
814 		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.
815 
816 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
817 
818 		History:
819 			Added November 23, 2021 (dub v10.4)
820 		See_Also: [enabled], [disabledBy]
821 	+/
822 	@property string disabledReason() {
823 		auto w = disabledBy();
824 		return (w is null) ? null : w.disabledReason_;
825 	}
826 
827 	/// ditto
828 	@property void disabledReason(string reason) {
829 		disabledReason_ = reason;
830 	}
831 
832 	/++
833 		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.
834 
835 		History:
836 			Added November 25, 2021 (dub v10.4)
837 		See_Also: [enabled], [disabledReason]
838 	+/
839 	Widget disabledBy() {
840 		Widget p = this;
841 		while(p) {
842 			if(!p._enabled)
843 				return p;
844 			p = p.parent;
845 		}
846 		return null;
847 	}
848 
849 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
850 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
851 		if(valueIsJson)
852 			return SetPropertyResult.wrongFormat;
853 		switch(name) {
854 			case "name":
855 				this.name = value.idup;
856 				return SetPropertyResult.success;
857 			case "statusTip":
858 				this.statusTip = value.idup;
859 				return SetPropertyResult.success;
860 			default:
861 				return SetPropertyResult.noSuchProperty;
862 		}
863 	}
864 	/// ditto
865 	void getPropertiesList(scope void delegate(string name) sink) const {
866 		sink("name");
867 		sink("statusTip");
868 	}
869 	/// ditto
870 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
871 		switch(name) {
872 			case "name":
873 				sink(name, this.name, false);
874 				return;
875 			case "statusTip":
876 				sink(name, this.statusTip, false);
877 				return;
878 			default:
879 				sink(name, null, true);
880 		}
881 	}
882 
883 	/++
884 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
885 
886 		History:
887 			Added November 25, 2021 (dub v10.5)
888 			`Point` overload added January 12, 2022 (dub v10.6)
889 	+/
890 	int scaleWithDpi(int value, int assumedDpi = 96) {
891 		// avoid potential overflow with common special values
892 		if(value == int.max)
893 			return int.max;
894 		if(value == int.min)
895 			return int.min;
896 		if(value == 0)
897 			return 0;
898 		return value * currentDpi(assumedDpi) / assumedDpi;
899 	}
900 
901 	/// ditto
902 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
903 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
904 	}
905 
906 	/++
907 		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.
908 
909 		Not entirely stable.
910 
911 		History:
912 			Added August 25, 2023 (dub v11.1)
913 	+/
914 	final int currentDpi(int assumedDpi = 96) {
915 		// assert(parentWindow !is null);
916 		// assert(parentWindow.win !is null);
917 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
918 		//divide = 138; // to test 1.5x
919 		// 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.
920 		// this also covers the case when actualDpi returns 0.
921 		if(divide < 96)
922 			divide = 96;
923 		return divide;
924 	}
925 
926 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
927 	// I'll think up something better eventually
928 
929 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
930 	protected final int defaultLineHeight() {
931 		auto cs = getComputedStyle();
932 		if(cs.font && !cs.font.isNull)
933 			return cs.font.height() * 5 / 4;
934 		else
935 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
936 	}
937 
938 	/++
939 
940 		History:
941 			Added August 25, 2023 (dub v11.1)
942 	+/
943 	protected final int defaultTextHeight(int numberOfLines = 1) {
944 		auto cs = getComputedStyle();
945 		if(cs.font && !cs.font.isNull)
946 			return cs.font.height() * numberOfLines;
947 		else
948 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
949 	}
950 
951 	protected final int defaultTextWidth(const(char)[] text) {
952 		auto cs = getComputedStyle();
953 		if(cs.font && !cs.font.isNull)
954 			return cs.font.stringWidth(text);
955 		else
956 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
957 	}
958 
959 	/++
960 		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.
961 
962 		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.
963 
964 		History:
965 			Added May 22, 2021
966 	+/
967 	protected bool encapsulatedChildren() {
968 		return false;
969 	}
970 
971 	private void privateDpiChanged() {
972 		dpiChanged();
973 		foreach(child; children)
974 			child.privateDpiChanged();
975 	}
976 
977 	/++
978 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
979 
980 		History:
981 			Added January 12, 2022 (dub v10.6)
982 	+/
983 	protected void dpiChanged() {
984 
985 	}
986 
987 	// Default layout properties {
988 
989 		int minWidth() { return 0; }
990 		int minHeight() {
991 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
992 			int sum = this.paddingTop + this.paddingBottom;
993 			foreach(child; children) {
994 				if(child.hidden)
995 					continue;
996 				sum += child.minHeight();
997 				sum += child.marginTop();
998 				sum += child.marginBottom();
999 			}
1000 
1001 			return sum;
1002 		}
1003 		int maxWidth() { return int.max; }
1004 		int maxHeight() { return int.max; }
1005 		int widthStretchiness() { return 4; }
1006 		int heightStretchiness() { return 4; }
1007 
1008 		/++
1009 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
1010 
1011 			History:
1012 				Added June 15, 2021 (dub v10.1)
1013 		+/
1014 		int widthShrinkiness() { return 0; }
1015 		/// ditto
1016 		int heightShrinkiness() { return 0; }
1017 
1018 		/++
1019 			The initial size of the widget for layout calculations. Default is 0.
1020 
1021 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
1022 
1023 			History:
1024 				Added June 15, 2021 (dub v10.1)
1025 		+/
1026 		int flexBasisWidth() { return 0; }
1027 		/// ditto
1028 		int flexBasisHeight() { return 0; }
1029 
1030 		/++
1031 			Not stable.
1032 
1033 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
1034 
1035 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
1036 
1037 			History:
1038 				Added January 5, 2023
1039 		+/
1040 		Rectangle defaultMargin;
1041 		/// ditto
1042 		Rectangle defaultPadding;
1043 
1044 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
1045 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
1046 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
1047 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
1048 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
1049 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
1050 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
1051 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
1052 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
1053 
1054 		private bool recomputeChildLayoutRequired = true;
1055 		private static class RecomputeEvent {}
1056 		private __gshared rce = new RecomputeEvent();
1057 		protected final void queueRecomputeChildLayout() {
1058 			recomputeChildLayoutRequired = true;
1059 
1060 			if(this.parentWindow) {
1061 				auto sw = this.parentWindow.win;
1062 				assert(sw !is null);
1063 				if(!sw.eventQueued!RecomputeEvent) {
1064 					sw.postEvent(rce);
1065 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1066 				}
1067 			}
1068 
1069 		}
1070 
1071 		protected final void recomputeChildLayoutEntry() {
1072 			if(recomputeChildLayoutRequired) {
1073 				recomputeChildLayout();
1074 				recomputeChildLayoutRequired = false;
1075 				redraw();
1076 			} else {
1077 				// I still need to check the tree just in case one of them was queued up
1078 				// and the event came up here instead of there.
1079 				foreach(child; children)
1080 					child.recomputeChildLayoutEntry();
1081 			}
1082 		}
1083 
1084 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
1085 		void recomputeChildLayout() {
1086 			.recomputeChildLayout!"height"(this);
1087 		}
1088 
1089 	// }
1090 
1091 
1092 	/++
1093 		Returns the style's tag name string this object uses.
1094 
1095 		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.
1096 
1097 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
1098 
1099 		History:
1100 			Added May 10, 2021
1101 	+/
1102 	string styleTagName() const {
1103 		string n = typeid(this).name;
1104 		foreach_reverse(idx, ch; n)
1105 			if(ch == '.') {
1106 				n = n[idx + 1 .. $];
1107 				break;
1108 			}
1109 		return n;
1110 	}
1111 
1112 	/// API for the [styleClassList]
1113 	static struct ClassList {
1114 		private Widget widget;
1115 
1116 		///
1117 		void add(string s) {
1118 			widget.styleClassList_ ~= s;
1119 		}
1120 
1121 		///
1122 		void remove(string s) {
1123 			foreach(idx, s1; widget.styleClassList_)
1124 				if(s1 == s) {
1125 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
1126 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
1127 					widget.styleClassList_.assumeSafeAppend();
1128 					return;
1129 				}
1130 		}
1131 
1132 		/// Returns true if it was added, false if it was removed.
1133 		bool toggle(string s) {
1134 			if(contains(s)) {
1135 				remove(s);
1136 				return false;
1137 			} else {
1138 				add(s);
1139 				return true;
1140 			}
1141 		}
1142 
1143 		///
1144 		bool contains(string s) const {
1145 			foreach(s1; widget.styleClassList_)
1146 				if(s1 == s)
1147 					return true;
1148 			return false;
1149 
1150 		}
1151 	}
1152 
1153 	private string[] styleClassList_;
1154 
1155 	/++
1156 		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.
1157 
1158 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
1159 
1160 		History:
1161 			Added May 10, 2021
1162 	+/
1163 	inout(ClassList) styleClassList() inout {
1164 		return cast(inout(ClassList)) ClassList(cast() this);
1165 	}
1166 
1167 	/++
1168 		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.
1169 
1170 		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.
1171 
1172 		The upper 32 bits are available for your own extensions.
1173 
1174 		History:
1175 			Added May 10, 2021
1176 	+/
1177 	enum DynamicState : ulong {
1178 		focus = (1 << 0), /// the widget currently has the keyboard focus
1179 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
1180 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!)
1181 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!)
1182 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
1183 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
1184 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
1185 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
1186 		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.
1187 
1188 		USER_BEGIN = (1UL << 32),
1189 	}
1190 
1191 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
1192 
1193 	/// ditto
1194 	@property ulong dynamicState() { return dynamicState_; }
1195 	/// ditto
1196 	@property ulong dynamicState(ulong newValue) {
1197 		if(dynamicState != newValue) {
1198 			auto old = dynamicState_;
1199 			dynamicState_ = newValue;
1200 
1201 			useStyleProperties((scope Widget.Style s) {
1202 				if(s.variesWithState(old ^ newValue))
1203 					redraw();
1204 			});
1205 		}
1206 		return dynamicState_;
1207 	}
1208 
1209 	/// ditto
1210 	void setDynamicState(ulong flags, bool state) {
1211 		auto ds = dynamicState_;
1212 		if(state)
1213 			ds |= flags;
1214 		else
1215 			ds &= ~flags;
1216 
1217 		dynamicState = ds;
1218 	}
1219 
1220 	private ulong dynamicState_;
1221 
1222 	deprecated("Use dynamic styles instead now") {
1223 		Color backgroundColor() { return backgroundColor_; }
1224 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
1225 
1226 		MouseCursor cursor() { return GenericCursor.Default; }
1227 	} private Color backgroundColor_ = Color.transparent;
1228 
1229 
1230 	/++
1231 		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).
1232 
1233 		It is here so there can be a specificity switch.
1234 
1235 		See [OverrideStyle] for a helper function to use your own.
1236 
1237 		History:
1238 			Added May 11, 2021
1239 	+/
1240 	static class Style/* : StyleProperties*/ {
1241 		public Widget widget; // public because the mixin template needs access to it
1242 
1243 		/++
1244 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
1245 
1246 			History:
1247 				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.
1248 		+/
1249 		bool variesWithState(ulong dynamicStateFlags) {
1250 			version(win32_widgets) {
1251 				if(widget.hwnd)
1252 					return false;
1253 			}
1254 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
1255 		}
1256 
1257 		///
1258 		Color foregroundColor() {
1259 			return WidgetPainter.visualTheme.foregroundColor;
1260 		}
1261 
1262 		///
1263 		WidgetBackground background() {
1264 			// the default is a "transparent" background, which means
1265 			// it goes as far up as it can to get the color
1266 			if (widget.backgroundColor_ != Color.transparent)
1267 				return WidgetBackground(widget.backgroundColor_);
1268 			if (widget.parent)
1269 				return widget.parent.getComputedStyle.background;
1270 			return WidgetBackground(widget.backgroundColor_);
1271 		}
1272 
1273 		private static OperatingSystemFont fontCached_;
1274 		private OperatingSystemFont fontCached() {
1275 			if(fontCached_ is null)
1276 				fontCached_ = font();
1277 			return fontCached_;
1278 		}
1279 
1280 		/++
1281 			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.
1282 		+/
1283 		OperatingSystemFont font() {
1284 			return null;
1285 		}
1286 
1287 		/++
1288 			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.
1289 
1290 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1291 
1292 			History:
1293 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1294 		+/
1295 		MouseCursor cursor() {
1296 			return GenericCursor.Default;
1297 		}
1298 
1299 		FrameStyle borderStyle() {
1300 			return FrameStyle.none;
1301 		}
1302 
1303 		/++
1304 		+/
1305 		Color borderColor() {
1306 			return Color.transparent;
1307 		}
1308 
1309 		FrameStyle outlineStyle() {
1310 			if(widget.dynamicState & DynamicState.focus)
1311 				return FrameStyle.dotted;
1312 			else
1313 				return FrameStyle.none;
1314 		}
1315 
1316 		Color outlineColor() {
1317 			return foregroundColor;
1318 		}
1319 	}
1320 
1321 	/++
1322 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1323 		The basic usage is simple:
1324 
1325 		---
1326 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1327 			// override style hints as-needed here
1328 		}
1329 		OverrideStyle!Style; // add the method
1330 		---
1331 
1332 		$(TIP
1333 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1334 			can not be inherited by other objects whereas the static one can. A property on the base class,
1335 			called [Widget.Style.widget|widget], is available for you to access its properties.
1336 		)
1337 
1338 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1339 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1340 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1341 
1342 
1343 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1344 		You may also just override `variesWithState` when you use this flag.
1345 
1346 		---
1347 		mixin OverrideStyle!(
1348 			DynamicState.focus, YourFocusedStyle,
1349 			DynamicState.hover, YourHoverStyle,
1350 			YourDefaultStyle
1351 		)
1352 		---
1353 
1354 		It checks if `dynamicState` matches the state and if so, returns the object given.
1355 
1356 		If there is no state mask given, the next one matches everything. The first match given is used.
1357 
1358 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1359 		find much use for this whole-class swap out.
1360 
1361 		History:
1362 			Added May 16, 2021
1363 	+/
1364 	static protected mixin template OverrideStyle(S...) {
1365 		static import amg = arsd.minigui;
1366 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1367 			ulong mask = 0;
1368 			foreach(idx, thing; S) {
1369 				static if(is(typeof(thing) : ulong)) {
1370 					mask = thing;
1371 				} else {
1372 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1373 						//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.");
1374 						scope amg.Widget.Style s = new thing();
1375 						s.widget = this;
1376 						dg(s);
1377 						return;
1378 					}
1379 				}
1380 			}
1381 		}
1382 	}
1383 	/++
1384 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1385 	+/
1386 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1387 		scope Style s = new Style();
1388 		s.widget = this;
1389 		dg(s);
1390 	}
1391 
1392 
1393 	protected void sendResizeEvent() {
1394 		this.emit!ResizeEvent();
1395 	}
1396 
1397 	/++
1398 		Override this to provide a custom context menu for your widget. (x, y) is where the menu was requested. If x == -1 && y == -1, the menu was triggered by the keyboard instead of the mouse and it should use the current cursor, selection, or whatever would make sense for where a keyboard user's attention would currently be.
1399 
1400 		It should return an instance of the [Menu] object. You may choose to cache this object. To construct one, either make `new Menu("", this);` (the empty string there is the menu's label, but for a context menu, that is not important), then call the `menu.addItem(new Action("Label Text", 0 /* icon id */, () { on clicked handler }), menu);` and `menu.addSeparator() methods, or use `return createContextMenuFromAnnotatedCode(this, some_command_struct);`
1401 
1402 		Context menus are automatically triggered by default by the keyboard menu key, mouse right click, and possibly other conventions per platform. You can also invoke one by calling the [showContextMenu] method.
1403 
1404 		See_Also:
1405 			[createContextMenuFromAnnotatedCode]
1406 	+/
1407 	Menu contextMenu(int x, int y) { return null; }
1408 
1409 	/++
1410 		Shows the widget's context menu, as if the user right clicked at the x, y position. You should rarely, if ever, have to call this, since default event handlers will do it for you automatically. To control what menu shows up, override [contextMenu] instead.
1411 	+/
1412 	final bool showContextMenu(int x, int y) {
1413 		return showContextMenu(x, y, -2, -2);
1414 	}
1415 
1416 	private final bool showContextMenu(int x, int y, int screenX, int screenY) {
1417 		if(parentWindow is null || parentWindow.win is null) return false;
1418 
1419 		auto menu = this.contextMenu(x, y);
1420 		if(menu is null)
1421 			return false;
1422 
1423 		version(win32_widgets) {
1424 			// FIXME: if it is -1, -1, do it at the current selection location instead
1425 			// tho the corner of the window, which it does now, isn't the literal worst.
1426 
1427 			// i see notepad just seems to put it in the center of the window so idk
1428 
1429 			if(screenX < 0 && screenY < 0) {
1430 				auto p = this.globalCoordinates();
1431 				if(screenX == -2)
1432 					p.x += x;
1433 				if(screenY == -2)
1434 					p.y += y;
1435 
1436 				screenX = p.x;
1437 				screenY = p.y;
1438 			}
1439 
1440 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1441 				throw new Exception("TrackContextMenuEx");
1442 		} else version(custom_widgets) {
1443 			menu.popup(this, x, y);
1444 		}
1445 
1446 		return true;
1447 	}
1448 
1449 	/++
1450 		Removes this widget from its parent.
1451 
1452 		History:
1453 			`removeWidget` was made `final` on May 11, 2021.
1454 	+/
1455 	@scriptable
1456 	final void removeWidget() {
1457 		auto p = this.parent;
1458 		if(p) {
1459 			int item;
1460 			for(item = 0; item < p._children.length; item++)
1461 				if(p._children[item] is this)
1462 					break;
1463 			auto idx = item;
1464 			for(; item < p._children.length - 1; item++)
1465 				p._children[item] = p._children[item + 1];
1466 			p._children = p._children[0 .. $-1];
1467 
1468 			this.parent.widgetRemoved(idx, this);
1469 			//this.parent = null;
1470 
1471 			p.queueRecomputeChildLayout();
1472 		}
1473 		version(win32_widgets) {
1474 			removeAllChildren();
1475 			if(hwnd) {
1476 				DestroyWindow(hwnd);
1477 				hwnd = null;
1478 			}
1479 		}
1480 	}
1481 
1482 	/++
1483 		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.
1484 
1485 		History:
1486 			Added September 19, 2021
1487 	+/
1488 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1489 
1490 	/++
1491 		Removes all child widgets from `this`. You should not use the removed widgets again.
1492 
1493 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1494 
1495 		History:
1496 			Added July 1, 2021 (dub v10.2)
1497 	+/
1498 	void removeAllChildren() {
1499 		version(win32_widgets)
1500 		foreach(child; _children) {
1501 			child.removeAllChildren();
1502 			if(child.hwnd) {
1503 				DestroyWindow(child.hwnd);
1504 				child.hwnd = null;
1505 			}
1506 		}
1507 		auto orig = this._children;
1508 		this._children = null;
1509 		foreach(idx, w; orig)
1510 			this.widgetRemoved(idx, w);
1511 
1512 		queueRecomputeChildLayout();
1513 	}
1514 
1515 	/++
1516 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1517 	+/
1518 	@scriptable
1519 	Widget getChildByName(string name) {
1520 		return getByName(name);
1521 	}
1522 	/++
1523 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1524 	+/
1525 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1526 		if(this.name == name)
1527 			if(auto c = cast(WidgetClass) this)
1528 				return c;
1529 		foreach(child; children) {
1530 			auto w = child.getByName(name);
1531 			if(auto c = cast(WidgetClass) w)
1532 				return c;
1533 		}
1534 		return null;
1535 	}
1536 
1537 	/++
1538 		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.
1539 		Names should be unique in a window.
1540 
1541 		See_Also: [getByName], [getChildByName]
1542 	+/
1543 	@scriptable string name;
1544 
1545 	private EventHandler[][string] bubblingEventHandlers;
1546 	private EventHandler[][string] capturingEventHandlers;
1547 
1548 	/++
1549 		Default event handlers. These are called on the appropriate
1550 		event unless [Event.preventDefault] is called on the event at
1551 		some point through the bubbling process.
1552 
1553 
1554 		If you are implementing your own widget and want to add custom
1555 		events, you should follow the same pattern here: create a virtual
1556 		function named `defaultEventHandler_eventname` with the implementation,
1557 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1558 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1559 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1560 		This ensures virtual dispatch based on the correct subclass.
1561 
1562 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1563 		overridden version.
1564 
1565 		You only need to do that on parent classes adding NEW event types. If you
1566 		just want to change the default behavior of an existing event type in a subclass,
1567 		you override the function (and optionally call `super.method_name`) like normal.
1568 
1569 	+/
1570 	protected EventHandler[string] defaultEventHandlers;
1571 
1572 	/// ditto
1573 	void setupDefaultEventHandlers() {
1574 		defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); };
1575 		defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); };
1576 		defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); };
1577 		defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); };
1578 		defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); };
1579 		defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); };
1580 		defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); };
1581 		defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); };
1582 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); };
1583 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); };
1584 		defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); };
1585 		defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); };
1586 		defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); };
1587 		defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); };
1588 		defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); };
1589 		defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); };
1590 		defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); };
1591 		defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); };
1592 	}
1593 
1594 	/// ditto
1595 	void defaultEventHandler_click(ClickEvent event) {}
1596 	/// ditto
1597 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1598 	/// ditto
1599 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1600 	/// ditto
1601 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1602 	/// ditto
1603 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1604 		if(event.button == MouseButton.left) {
1605 			if(this.tabStop) {
1606 				this.focus();
1607 			}
1608 		} else if(event.button == MouseButton.right) {
1609 			showContextMenu(event.clientX, event.clientY);
1610 		}
1611 	}
1612 	/// ditto
1613 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1614 	/// ditto
1615 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1616 	/// ditto
1617 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1618 	/// ditto
1619 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1620 	/// ditto
1621 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1622 	/// ditto
1623 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1624 	/// ditto
1625 	void defaultEventHandler_char(CharEvent event) {}
1626 	/// ditto
1627 	void defaultEventHandler_triggered(Event event) {}
1628 	/// ditto
1629 	void defaultEventHandler_change(Event event) {}
1630 	/// ditto
1631 	void defaultEventHandler_focus(Event event) {}
1632 	/// ditto
1633 	void defaultEventHandler_blur(Event event) {}
1634 	/// ditto
1635 	void defaultEventHandler_focusin(Event event) {}
1636 	/// ditto
1637 	void defaultEventHandler_focusout(Event event) {}
1638 
1639 	/++
1640 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1641 
1642 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1643 
1644 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1645 		of participating in handler delegation.
1646 
1647 		$(TIP
1648 			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.
1649 		)
1650 	+/
1651 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1652 		return addEventListener(event, (Widget, scope Event e) {
1653 			if(e.srcElement is this)
1654 				handler();
1655 		}, useCapture);
1656 	}
1657 
1658 	/// ditto
1659 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1660 		return addEventListener(event, (Widget, Event e) {
1661 			if(e.srcElement is this)
1662 				handler(e);
1663 		}, useCapture);
1664 	}
1665 
1666 	/// ditto
1667 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1668 		static if(is(Handler Fn == delegate)) {
1669 		static if(is(Fn Params == __parameters)) {
1670 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1671 				if(e.srcElement !is this)
1672 					return;
1673 				auto ty = cast(Params[0]) e;
1674 				if(ty !is null)
1675 					handler(ty);
1676 			}, useCapture);
1677 		} else static assert(0);
1678 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1679 	}
1680 
1681 	/// ditto
1682 	@scriptable
1683 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1684 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1685 	}
1686 
1687 	/// ditto
1688 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1689 		static if(is(Handler Fn == delegate)) {
1690 		static if(is(Fn Params == __parameters)) {
1691 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1692 				auto ty = cast(Params[0]) e;
1693 				if(ty !is null)
1694 					handler(ty);
1695 			}, useCapture);
1696 		} else static assert(0);
1697 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1698 	}
1699 
1700 	/// ditto
1701 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1702 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1703 	}
1704 
1705 	/// ditto
1706 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1707 		if(event.length > 2 && event[0..2] == "on")
1708 			event = event[2 .. $];
1709 
1710 		if(useCapture)
1711 			capturingEventHandlers[event] ~= handler;
1712 		else
1713 			bubblingEventHandlers[event] ~= handler;
1714 
1715 		return EventListener(this, event, handler, useCapture);
1716 	}
1717 
1718 	/// ditto
1719 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1720 		if(event.length > 2 && event[0..2] == "on")
1721 			event = event[2 .. $];
1722 
1723 		if(useCapture) {
1724 			if(event in capturingEventHandlers)
1725 			foreach(ref evt; capturingEventHandlers[event])
1726 				if(evt is handler) evt = null;
1727 		} else {
1728 			if(event in bubblingEventHandlers)
1729 			foreach(ref evt; bubblingEventHandlers[event])
1730 				if(evt is handler) evt = null;
1731 		}
1732 	}
1733 
1734 	/// ditto
1735 	void removeEventListener(EventListener listener) {
1736 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1737 	}
1738 
1739 	static if(UsingSimpledisplayX11) {
1740 		void discardXConnectionState() {
1741 			foreach(child; children)
1742 				child.discardXConnectionState();
1743 		}
1744 
1745 		void recreateXConnectionState() {
1746 			foreach(child; children)
1747 				child.recreateXConnectionState();
1748 			redraw();
1749 		}
1750 	}
1751 
1752 	/++
1753 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1754 
1755 		History:
1756 			`globalCoordinates` was made `final` on May 11, 2021.
1757 	+/
1758 	Point globalCoordinates() {
1759 		int x = this.x;
1760 		int y = this.y;
1761 		auto p = this.parent;
1762 		while(p) {
1763 			x += p.x;
1764 			y += p.y;
1765 			p = p.parent;
1766 		}
1767 
1768 		static if(UsingSimpledisplayX11) {
1769 			auto dpy = XDisplayConnection.get;
1770 			arsd.simpledisplay.Window dummyw;
1771 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1772 		} else version(Windows) {
1773 			POINT pt;
1774 			pt.x = x;
1775 			pt.y = y;
1776 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1777 			x = pt.x;
1778 			y = pt.y;
1779 		} else {
1780 			featureNotImplemented();
1781 		}
1782 
1783 		return Point(x, y);
1784 	}
1785 
1786 	version(win32_widgets)
1787 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1788 
1789 	version(win32_widgets)
1790 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1791 	void handleWmCommand(ushort cmd, ushort id) {}
1792 
1793 	version(win32_widgets)
1794 	/++
1795 		Called when a WM_NOTIFY is sent to the associated hwnd.
1796 
1797 		History:
1798 	+/
1799 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1800 
1801 	version(win32_widgets)
1802 	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); }
1803 
1804 	/++
1805 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1806 
1807 		Updates to this variable will only be made visible on the next mouse enter event.
1808 	+/
1809 	@scriptable string statusTip;
1810 	// string toolTip;
1811 	// string helpText;
1812 
1813 	/++
1814 		If true, this widget can be focused via keyboard control with the tab key.
1815 
1816 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1817 	+/
1818 	bool tabStop = true;
1819 	/++
1820 		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.)
1821 	+/
1822 	int tabOrder;
1823 
1824 	version(win32_widgets) {
1825 		static Widget[HWND] nativeMapping;
1826 		/// The native handle, if there is one.
1827 		HWND hwnd;
1828 		WNDPROC originalWindowProcedure;
1829 
1830 		SimpleWindow simpleWindowWrappingHwnd;
1831 
1832 		// please note it IGNORES your return value and does NOT forward it to Windows!
1833 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1834 			return 0;
1835 		}
1836 	}
1837 	private bool implicitlyCreated;
1838 
1839 	/// 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.
1840 	int x;
1841 	/// ditto
1842 	int y;
1843 	private int _width;
1844 	private int _height;
1845 	private Widget[] _children;
1846 	private Widget _parent;
1847 	private Window _parentWindow;
1848 
1849 	/++
1850 		Returns the window to which this widget is attached.
1851 
1852 		History:
1853 			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.
1854 	+/
1855 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1856 	private @property void parentWindow(Window parent) {
1857 		auto old = _parentWindow;
1858 		_parentWindow = parent;
1859 		newParentWindow(old, _parentWindow);
1860 		foreach(child; children)
1861 			child.parentWindow = parent; // please note that this is recursive
1862 	}
1863 
1864 	/++
1865 		Called when the widget has been added to or remove from a parent window.
1866 
1867 		Note that either oldParent and/or newParent may be null any time this is called.
1868 
1869 		History:
1870 			Added September 13, 2024
1871 	+/
1872 	protected void newParentWindow(Window oldParent, Window newParent) {}
1873 
1874 	/++
1875 		Returns the list of the widget's children.
1876 
1877 		History:
1878 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1879 
1880 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1881 	+/
1882 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1883 
1884 	/++
1885 		Returns the widget's parent.
1886 
1887 		History:
1888 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1889 
1890 			The parent should only be managed by the [addChild] and [removeWidget] method.
1891 	+/
1892 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1893 
1894 	/// The widget's current size.
1895 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1896 	/// ditto
1897 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1898 
1899 	/// Only the layout manager should be calling these.
1900 	final protected @property int width(int a) @safe { return _width = a; }
1901 	/// ditto
1902 	final protected @property int height(int a) @safe { return _height = a; }
1903 
1904 	/++
1905 		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.
1906 
1907 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
1908 	+/
1909 	protected void registerMovement() {
1910 		version(win32_widgets) {
1911 			if(hwnd) {
1912 				auto pos = getChildPositionRelativeToParentHwnd(this);
1913 				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
1914 				this.redraw();
1915 			}
1916 		}
1917 		sendResizeEvent();
1918 	}
1919 
1920 	/// Creates the widget and adds it to the parent.
1921 	this(Widget parent) {
1922 		if(parent !is null)
1923 			parent.addChild(this);
1924 		setupDefaultEventHandlers();
1925 	}
1926 
1927 	/// 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.
1928 	@scriptable
1929 	bool isFocused() {
1930 		return parentWindow && parentWindow.focusedWidget is this;
1931 	}
1932 
1933 	private bool showing_ = true;
1934 	///
1935 	bool showing() const { return showing_; }
1936 	///
1937 	bool hidden() const { return !showing_; }
1938 	/++
1939 		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.
1940 
1941 		Note that a widget only ever shows if all its parents are showing too.
1942 	+/
1943 	void showing(bool s, bool recalculate = true) {
1944 		if(s != showing_) {
1945 			showing_ = s;
1946 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
1947 
1948 			showNativeWindowChildren(s);
1949 
1950 			if(parent && recalculate) {
1951 				parent.queueRecomputeChildLayout();
1952 				parent.redraw();
1953 			}
1954 
1955 			if(s) {
1956 				queueRecomputeChildLayout();
1957 				redraw();
1958 			}
1959 		}
1960 	}
1961 	/// Convenience method for `showing = true`
1962 	@scriptable
1963 	void show() {
1964 		showing = true;
1965 	}
1966 	/// Convenience method for `showing = false`
1967 	@scriptable
1968 	void hide() {
1969 		showing = false;
1970 	}
1971 
1972 	/++
1973 		If you are a native window, show/hide it based on shouldShow and return `true`.
1974 
1975 		Otherwise, do nothing and return false.
1976 	+/
1977 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
1978 		version(win32_widgets) {
1979 			if(hwnd) {
1980 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
1981 				return true;
1982 			} else {
1983 				return false;
1984 			}
1985 		} else {
1986 			return false;
1987 		}
1988 	}
1989 
1990 	private void showNativeWindowChildren(bool s) {
1991 		if(!showOrHideIfNativeWindow(s && showing))
1992 			foreach(child; children)
1993 				child.showNativeWindowChildren(s);
1994 	}
1995 
1996 	///
1997 	@scriptable
1998 	void focus() {
1999 		assert(parentWindow !is null);
2000 		if(isFocused())
2001 			return;
2002 
2003 		if(parentWindow.focusedWidget) {
2004 			// FIXME: more details here? like from and to
2005 			auto from = parentWindow.focusedWidget;
2006 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
2007 			parentWindow.focusedWidget = null;
2008 			from.emit!BlurEvent();
2009 			from.emit!FocusOutEvent();
2010 		}
2011 
2012 
2013 		version(win32_widgets) {
2014 			if(this.hwnd !is null)
2015 				SetFocus(this.hwnd);
2016 		}
2017 		//else static if(UsingSimpledisplayX11)
2018 			//this.parentWindow.win.focus();
2019 
2020 		parentWindow.focusedWidget = this;
2021 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
2022 		this.emit!FocusEvent();
2023 		this.emit!FocusInEvent();
2024 	}
2025 
2026 	/+
2027 	/++
2028 		Unfocuses the widget. This may reset
2029 	+/
2030 	@scriptable
2031 	void blur() {
2032 
2033 	}
2034 	+/
2035 
2036 
2037 	/++
2038 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
2039 
2040 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
2041 	+/
2042 	void attachedToWindow(Window w) {}
2043 	/++
2044 		Callback when the widget is added to another widget.
2045 
2046 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
2047 	+/
2048 	void addedTo(Widget w) {}
2049 
2050 	/++
2051 		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.
2052 
2053 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
2054 	+/
2055 	protected void addChild(Widget w, int position = int.max) {
2056 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
2057 		assert(w !is this, "Child cannot be its own parent!");
2058 		w._parent = this;
2059 		if(position == int.max || position == children.length) {
2060 			_children ~= w;
2061 		} else {
2062 			assert(position < _children.length);
2063 			_children.length = _children.length + 1;
2064 			for(int i = cast(int) _children.length - 1; i > position; i--)
2065 				_children[i] = _children[i - 1];
2066 			_children[position] = w;
2067 		}
2068 
2069 		this.parentWindow = this._parentWindow;
2070 
2071 		w.addedTo(this);
2072 
2073 		bool parentIsNative;
2074 		version(win32_widgets) {
2075 			parentIsNative = hwnd !is null;
2076 		}
2077 		if(!parentIsNative && !showing)
2078 			w.showOrHideIfNativeWindow(false);
2079 
2080 		if(parentWindow !is null) {
2081 			w.attachedToWindow(parentWindow);
2082 			parentWindow.queueRecomputeChildLayout();
2083 			parentWindow.redraw();
2084 		}
2085 	}
2086 
2087 	/++
2088 		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.
2089 	+/
2090 	Widget getChildAtPosition(int x, int y) {
2091 		// it goes backward so the last one to show gets picked first
2092 		// might use z-index later
2093 		foreach_reverse(child; children) {
2094 			if(child.hidden)
2095 				continue;
2096 			if(child.x <= x && child.y <= y
2097 				&& ((x - child.x) < child.width)
2098 				&& ((y - child.y) < child.height))
2099 			{
2100 				return child;
2101 			}
2102 		}
2103 
2104 		return null;
2105 	}
2106 
2107 	/++
2108 		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.
2109 
2110 		History:
2111 			Added July 2, 2021 (v10.2)
2112 	+/
2113 	protected void addScrollPosition(ref int x, ref int y) {};
2114 
2115 	/++
2116 		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.
2117 
2118 		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.
2119 
2120 		[paint] is not called for system widgets as the OS library draws them instead.
2121 
2122 
2123 		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.
2124 
2125 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
2126 
2127 		History:
2128 			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.
2129 	+/
2130 	void paint(WidgetPainter painter) {
2131 		version(win32_widgets)
2132 			if(hwnd) {
2133 				return;
2134 			}
2135 		painter.drawThemed(&paintContent); // note this refers to the following overload
2136 	}
2137 
2138 	/++
2139 		Responsible for drawing the content as the theme engine is responsible for other elements.
2140 
2141 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
2142 
2143 		Params:
2144 			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.
2145 
2146 			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.
2147 
2148 			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.
2149 
2150 		Returns:
2151 			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.
2152 
2153 		History:
2154 			Added May 15, 2021
2155 	+/
2156 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2157 		return bounds;
2158 	}
2159 
2160 	deprecated("Change ScreenPainter to WidgetPainter")
2161 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
2162 
2163 	/// I don't actually like the name of this
2164 	/// this draws a background on it
2165 	void erase(WidgetPainter painter) {
2166 		version(win32_widgets)
2167 			if(hwnd) return; // Windows will do it. I think.
2168 
2169 		auto c = getComputedStyle().background.color;
2170 		painter.fillColor = c;
2171 		painter.outlineColor = c;
2172 
2173 		version(win32_widgets) {
2174 			HANDLE b, p;
2175 			if(c.a == 0 && parent is parentWindow) {
2176 				// I don't remember why I had this really...
2177 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
2178 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
2179 			}
2180 		}
2181 		painter.drawRectangle(Point(0, 0), width, height);
2182 		version(win32_widgets) {
2183 			if(c.a == 0 && parent is parentWindow) {
2184 				SelectObject(painter.impl.hdc, p);
2185 				SelectObject(painter.impl.hdc, b);
2186 			}
2187 		}
2188 	}
2189 
2190 	///
2191 	WidgetPainter draw() {
2192 		int x = this.x, y = this.y;
2193 		auto parent = this.parent;
2194 		while(parent) {
2195 			x += parent.x;
2196 			y += parent.y;
2197 			parent = parent.parent;
2198 		}
2199 
2200 		auto painter = parentWindow.win.draw(true);
2201 		painter.originX = x;
2202 		painter.originY = y;
2203 		painter.setClipRectangle(Point(0, 0), width, height);
2204 		return WidgetPainter(painter, this);
2205 	}
2206 
2207 	/// 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.
2208 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
2209 		if(hidden)
2210 			return;
2211 
2212 		int paintX = x;
2213 		int paintY = y;
2214 		if(this.useNativeDrawing()) {
2215 			paintX = 0;
2216 			paintY = 0;
2217 			lox = 0;
2218 			loy = 0;
2219 			containment = Rectangle(0, 0, int.max, int.max);
2220 		}
2221 
2222 		painter.originX = lox + paintX;
2223 		painter.originY = loy + paintY;
2224 
2225 		bool actuallyPainted = false;
2226 
2227 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
2228 		if(clip == Rectangle.init) {
2229 			// writeln(this, " clipped out");
2230 			return;
2231 		}
2232 
2233 		bool invalidateChildren = invalidate;
2234 
2235 		if(redrawRequested || force) {
2236 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
2237 
2238 			painter.drawingUpon = this;
2239 
2240 			erase(painter);
2241 			if(painter.visualTheme)
2242 				painter.visualTheme.doPaint(this, painter);
2243 			else
2244 				paint(painter);
2245 
2246 			if(invalidate) {
2247 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
2248 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
2249 				painter.invalidateRect(region);
2250 				// children are contained inside this, so no need to do extra work
2251 				invalidateChildren = false;
2252 			}
2253 
2254 			redrawRequested = false;
2255 			actuallyPainted = true;
2256 		}
2257 
2258 		foreach(child; children) {
2259 			version(win32_widgets)
2260 				if(child.useNativeDrawing()) continue;
2261 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
2262 		}
2263 
2264 		version(win32_widgets)
2265 		foreach(child; children) {
2266 			if(child.useNativeDrawing) {
2267 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
2268 				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
2269 			}
2270 		}
2271 	}
2272 
2273 	protected bool useNativeDrawing() nothrow {
2274 		version(win32_widgets)
2275 			return hwnd !is null;
2276 		else
2277 			return false;
2278 	}
2279 
2280 	private static class RedrawEvent {}
2281 	private __gshared re = new RedrawEvent();
2282 
2283 	private bool redrawRequested;
2284 	///
2285 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
2286 		redrawRequested = true;
2287 
2288 		if(this.parentWindow) {
2289 			auto sw = this.parentWindow.win;
2290 			assert(sw !is null);
2291 			if(!sw.eventQueued!RedrawEvent) {
2292 				sw.postEvent(re);
2293 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2294 			}
2295 		}
2296 	}
2297 
2298 	private SimpleWindow drawableWindow;
2299 
2300 	/++
2301 		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.
2302 
2303 		Returns:
2304 			`true` if you should do your default behavior.
2305 
2306 		History:
2307 			Added May 5, 2021
2308 
2309 		Bugs:
2310 			It does not do the static checks on gdc right now.
2311 	+/
2312 	final protected bool emit(EventType, this This, Args...)(Args args) {
2313 		version(GNU) {} else
2314 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2315 		auto e = new EventType(this, args);
2316 		e.dispatch();
2317 		return !e.defaultPrevented;
2318 	}
2319 	/// ditto
2320 	final protected bool emit(string eventString, this This)() {
2321 		auto e = new Event(eventString, this);
2322 		e.dispatch();
2323 		return !e.defaultPrevented;
2324 	}
2325 
2326 	/++
2327 		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.
2328 
2329 		History:
2330 			Added May 5, 2021
2331 	+/
2332 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2333 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2334 		return addEventListener(handler);
2335 	}
2336 
2337 	/++
2338 		Gets the computed style properties from the visual theme.
2339 
2340 		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].)
2341 
2342 		History:
2343 			Added May 8, 2021
2344 	+/
2345 	final StyleInformation getComputedStyle() {
2346 		return StyleInformation(this);
2347 	}
2348 
2349 	int focusableWidgets(scope int delegate(Widget) dg) {
2350 		foreach(widget; WidgetStream(this)) {
2351 			if(widget.tabStop && !widget.hidden) {
2352 				int result = dg(widget);
2353 				if (result)
2354 					return result;
2355 			}
2356 		}
2357 		return 0;
2358 	}
2359 
2360 	/++
2361 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2362 		for the given content box (the area between the padding)
2363 
2364 		History:
2365 			Added January 4, 2023 (dub v11.0)
2366 	+/
2367 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2368 		auto cs = getComputedStyle();
2369 
2370 		auto borderWidth = getBorderWidth(cs.borderStyle);
2371 
2372 		auto rect = contentBox;
2373 
2374 		rect.left -= borderWidth;
2375 		rect.right += borderWidth;
2376 		rect.top -= borderWidth;
2377 		rect.bottom += borderWidth;
2378 
2379 		auto insideBorderRect = rect;
2380 
2381 		rect.left -= cs.paddingLeft;
2382 		rect.right += cs.paddingRight;
2383 		rect.top -= cs.paddingTop;
2384 		rect.bottom += cs.paddingBottom;
2385 
2386 		return rect;
2387 	}
2388 
2389 
2390 	// FIXME: I kinda want to hide events from implementation widgets
2391 	// so it just catches them all and stops propagation...
2392 	// i guess i can do it with a event listener on star.
2393 
2394 	mixin Emits!KeyDownEvent; ///
2395 	mixin Emits!KeyUpEvent; ///
2396 	mixin Emits!CharEvent; ///
2397 
2398 	mixin Emits!MouseDownEvent; ///
2399 	mixin Emits!MouseUpEvent; ///
2400 	mixin Emits!ClickEvent; ///
2401 	mixin Emits!DoubleClickEvent; ///
2402 	mixin Emits!MouseMoveEvent; ///
2403 	mixin Emits!MouseOverEvent; ///
2404 	mixin Emits!MouseOutEvent; ///
2405 	mixin Emits!MouseEnterEvent; ///
2406 	mixin Emits!MouseLeaveEvent; ///
2407 
2408 	mixin Emits!ResizeEvent; ///
2409 
2410 	mixin Emits!BlurEvent; ///
2411 	mixin Emits!FocusEvent; ///
2412 
2413 	mixin Emits!FocusInEvent; ///
2414 	mixin Emits!FocusOutEvent; ///
2415 }
2416 
2417 /+
2418 /++
2419 	Interface to indicate that the widget has a simple value property.
2420 
2421 	History:
2422 		Added August 26, 2021
2423 +/
2424 interface HasValue!T {
2425 	/// Getter
2426 	@property T value();
2427 	/// Setter
2428 	@property void value(T);
2429 }
2430 
2431 /++
2432 	Interface to indicate that the widget has a range of possible values for its simple value property.
2433 	This would be present on something like a slider or possibly a number picker.
2434 
2435 	History:
2436 		Added September 11, 2021
2437 +/
2438 interface HasRangeOfValues!T : HasValue!T {
2439 	/// The minimum and maximum values in the range, inclusive.
2440 	@property T minValue();
2441 	@property void minValue(T); /// ditto
2442 	@property T maxValue(); /// ditto
2443 	@property void maxValue(T); /// ditto
2444 
2445 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2446 	@property void step(T);
2447 	@property T step(); /// ditto
2448 }
2449 
2450 /++
2451 	Interface to indicate that the widget has a list of possible values the user can choose from.
2452 	This would be present on something like a drop-down selector.
2453 
2454 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2455 	combobox.
2456 
2457 	History:
2458 		Added September 11, 2021
2459 +/
2460 interface HasListOfValues!T : HasValue!T {
2461 	@property T[] values;
2462 	@property void values(T[]);
2463 
2464 	@property int selectedIndex(); // note it may return -1!
2465 	@property void selectedIndex(int);
2466 }
2467 +/
2468 
2469 /++
2470 	History:
2471 		Added September 2021 (dub v10.4)
2472 +/
2473 class GridLayout : Layout {
2474 
2475 	// 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.
2476 
2477 	/++
2478 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2479 	+/
2480 	enum Gravity {
2481 		Center    = 0,
2482 		NorthWest = North | West,
2483 		North     = 0b10_00,
2484 		NorthEast = North | East,
2485 		West      = 0b00_10,
2486 		East      = 0b00_01,
2487 		SouthWest = South | West,
2488 		South     = 0b01_00,
2489 		SouthEast = South | East,
2490 	}
2491 
2492 	/++
2493 		The width and height are in some proportional units and can often just be 12.
2494 	+/
2495 	this(int width, int height, Widget parent) {
2496 		this.gridWidth = width;
2497 		this.gridHeight = height;
2498 		super(parent);
2499 	}
2500 
2501 	/++
2502 		Sets the position of the given child.
2503 
2504 		The units of these arguments are in the proportional grid units you set in the constructor.
2505 	+/
2506 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2507 		// ensure it is in bounds
2508 		// then ensure no overlaps
2509 
2510 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2511 
2512 		foreach(ref position; positions) {
2513 			if(position.widget is child) {
2514 				position = p;
2515 				goto set;
2516 			}
2517 		}
2518 
2519 		positions ~= p;
2520 
2521 		set:
2522 
2523 		// FIXME: should this batch?
2524 		queueRecomputeChildLayout();
2525 
2526 		return child;
2527 	}
2528 
2529 	override void addChild(Widget w, int position = int.max) {
2530 		super.addChild(w, position);
2531 		//positions ~= ChildPosition(w);
2532 		if(position != int.max) {
2533 			// FIXME: align it so they actually match.
2534 		}
2535 	}
2536 
2537 	override void widgetRemoved(size_t idx, Widget w) {
2538 		// FIXME: keep the positions array aligned
2539 		// positions[idx].widget = null;
2540 	}
2541 
2542 	override void recomputeChildLayout() {
2543 		registerMovement();
2544 		int onGrid = cast(int) positions.length;
2545 		c: foreach(child; children) {
2546 			// just snap it to the grid
2547 			if(onGrid)
2548 			foreach(position; positions)
2549 				if(position.widget is child) {
2550 					child.x = this.width * position.x / this.gridWidth;
2551 					child.y = this.height * position.y / this.gridHeight;
2552 					child.width = this.width * position.width / this.gridWidth;
2553 					child.height = this.height * position.height / this.gridHeight;
2554 
2555 					auto diff = child.width - child.maxWidth();
2556 					// FIXME: gravity?
2557 					if(diff > 0) {
2558 						child.width = child.width - diff;
2559 
2560 						if(position.gravity & Gravity.West) {
2561 							// nothing needed, already aligned
2562 						} else if(position.gravity & Gravity.East) {
2563 							child.x += diff;
2564 						} else {
2565 							child.x += diff / 2;
2566 						}
2567 					}
2568 
2569 					diff = child.height - child.maxHeight();
2570 					// FIXME: gravity?
2571 					if(diff > 0) {
2572 						child.height = child.height - diff;
2573 
2574 						if(position.gravity & Gravity.North) {
2575 							// nothing needed, already aligned
2576 						} else if(position.gravity & Gravity.South) {
2577 							child.y += diff;
2578 						} else {
2579 							child.y += diff / 2;
2580 						}
2581 					}
2582 					child.recomputeChildLayout();
2583 					onGrid--;
2584 					continue c;
2585 				}
2586 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2587 		}
2588 	}
2589 
2590 	private struct ChildPosition {
2591 		Widget widget;
2592 		int x;
2593 		int y;
2594 		int width;
2595 		int height;
2596 		Gravity gravity;
2597 	}
2598 	private ChildPosition[] positions;
2599 
2600 	int gridWidth = 12;
2601 	int gridHeight = 12;
2602 }
2603 
2604 ///
2605 abstract class ComboboxBase : Widget {
2606 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2607 	// or to always show the list, we want CBS_SIMPLE == 1
2608 	version(win32_widgets)
2609 		this(uint style, Widget parent) {
2610 			super(parent);
2611 			createWin32Window(this, "ComboBox"w, null, style);
2612 		}
2613 	else version(custom_widgets)
2614 		this(Widget parent) {
2615 			super(parent);
2616 
2617 			addEventListener((KeyDownEvent event) {
2618 				if(event.key == Key.Up) {
2619 					setSelection(selection_-1);
2620 					event.preventDefault();
2621 				}
2622 				if(event.key == Key.Down) {
2623 					setSelection(selection_+1);
2624 					event.preventDefault();
2625 				}
2626 
2627 			});
2628 
2629 		}
2630 	else static assert(false);
2631 
2632 	protected void scrollSelectionIntoView() {}
2633 
2634 	/++
2635 		Returns the current list of options in the selection.
2636 
2637 		History:
2638 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2639 	+/
2640 	final @property string[] options() const {
2641 		return cast(string[]) options_;
2642 	}
2643 
2644 	/++
2645 		Replaces the list of options in the box. Note that calling this will also reset the selection.
2646 
2647 		History:
2648 			Added December, 29 2024
2649 	+/
2650 	final @property void options(string[] options) {
2651 		version(win32_widgets)
2652 			SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0);
2653 		selection_ = -1;
2654 		options_ = null;
2655 		foreach(opt; options)
2656 			addOption(opt);
2657 
2658 		version(custom_widgets)
2659 			redraw();
2660 	}
2661 
2662 	private string[] options_;
2663 	private int selection_ = -1;
2664 
2665 	/++
2666 		Adds an option to the end of options array.
2667 	+/
2668 	void addOption(string s) {
2669 		options_ ~= s;
2670 		version(win32_widgets)
2671 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2672 	}
2673 
2674 	/++
2675 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2676 	+/
2677 	int getSelection() {
2678 		return selection_;
2679 	}
2680 
2681 	/++
2682 		Returns the current selection as a string.
2683 
2684 		History:
2685 			Added November 17, 2021
2686 	+/
2687 	string getSelectionString() {
2688 		return selection_ == -1 ? null : options[selection_];
2689 	}
2690 
2691 	/++
2692 		Sets the current selection to an index in the options array, or to the given option if present.
2693 		Please note that the string version may do a linear lookup.
2694 
2695 		Returns:
2696 			the index you passed in
2697 
2698 		History:
2699 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2700 
2701 			The return value was `void` prior to March 1, 2022.
2702 	+/
2703 	int setSelection(int idx) {
2704 		if(idx < -1)
2705 			idx = -1;
2706 		if(idx + 1 > options.length)
2707 			idx = cast(int) options.length - 1;
2708 
2709 		selection_ = idx;
2710 
2711 		version(win32_widgets)
2712 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2713 
2714 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2715 		t.dispatch();
2716 
2717 		scrollSelectionIntoView();
2718 
2719 		return idx;
2720 	}
2721 
2722 	/// ditto
2723 	int setSelection(string s) {
2724 		if(s !is null)
2725 		foreach(idx, item; options)
2726 			if(item == s) {
2727 				return setSelection(cast(int) idx);
2728 			}
2729 		return setSelection(-1);
2730 	}
2731 
2732 	/++
2733 		This event is fired when the selection changes. Note it inherits
2734 		from ChangeEvent!string, meaning you can use that as well, and it also
2735 		fills in [Event.intValue].
2736 	+/
2737 	static class SelectionChangedEvent : ChangeEvent!string {
2738 		this(Widget target, int iv, string sv) {
2739 			super(target, &stringValue);
2740 			this.iv = iv;
2741 			this.sv = sv;
2742 		}
2743 		immutable int iv;
2744 		immutable string sv;
2745 
2746 		override @property string stringValue() { return sv; }
2747 		override @property int intValue() { return iv; }
2748 	}
2749 
2750 	version(win32_widgets)
2751 	override void handleWmCommand(ushort cmd, ushort id) {
2752 		if(cmd == CBN_SELCHANGE) {
2753 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2754 			fireChangeEvent();
2755 		}
2756 	}
2757 
2758 	private void fireChangeEvent() {
2759 		if(selection_ >= options.length)
2760 			selection_ = -1;
2761 
2762 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2763 		t.dispatch();
2764 	}
2765 
2766 	override int minWidth() { return scaleWithDpi(32); }
2767 
2768 	version(win32_widgets) {
2769 		override int minHeight() { return defaultLineHeight + 6; }
2770 		override int maxHeight() { return defaultLineHeight + 6; }
2771 	} else {
2772 		override int minHeight() { return defaultLineHeight + 4; }
2773 		override int maxHeight() { return defaultLineHeight + 4; }
2774 	}
2775 
2776 	version(custom_widgets)
2777 	void popup() {
2778 		CustomComboBoxPopup popup = new CustomComboBoxPopup(this);
2779 	}
2780 
2781 }
2782 
2783 private class CustomComboBoxPopup : Window {
2784 	private ComboboxBase associatedWidget;
2785 	private ListWidget lw;
2786 	private bool cancelled;
2787 
2788 	this(ComboboxBase associatedWidget) {
2789 		this.associatedWidget = associatedWidget;
2790 
2791 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2792 
2793 		auto w = associatedWidget.width;
2794 		// FIXME: suggestedDropdownHeight see below
2795 		auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8);
2796 
2797 		// FIXME: this sux
2798 		if(h > associatedWidget.parentWindow.height)
2799 			h = associatedWidget.parentWindow.height;
2800 
2801 		auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok
2802 		if(h < mh)
2803 			h = mh;
2804 
2805 		auto coord = associatedWidget.globalCoordinates();
2806 		auto dropDown = new SimpleWindow(
2807 			w, h,
2808 			null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null);
2809 
2810 		super(dropDown);
2811 
2812 		dropDown.move(coord.x, coord.y + associatedWidget.height);
2813 
2814 		this.lw = new ListWidget(this);
2815 		version(custom_widgets)
2816 			lw.multiSelect = false;
2817 		foreach(option; associatedWidget.options)
2818 			lw.addOption(option);
2819 
2820 		auto originalSelection = associatedWidget.getSelection;
2821 		lw.setSelection(originalSelection);
2822 		lw.scrollSelectionIntoView();
2823 
2824 		/+
2825 		{
2826 			auto cs = getComputedStyle();
2827 			auto painter = dropDown.draw();
2828 			draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2829 			auto p = Point(4, 4);
2830 			painter.outlineColor = cs.foregroundColor;
2831 			foreach(option; associatedWidget.options) {
2832 				painter.drawText(p, option);
2833 				p.y += defaultLineHeight;
2834 			}
2835 		}
2836 
2837 		dropDown.setEventHandlers(
2838 			(MouseEvent event) {
2839 				if(event.type == MouseEventType.buttonReleased) {
2840 					dropDown.close();
2841 					auto element = (event.y - 4) / defaultLineHeight;
2842 					if(element >= 0 && element <= associatedWidget.options.length) {
2843 						associatedWidget.selection_ = element;
2844 
2845 						associatedWidget.fireChangeEvent();
2846 					}
2847 				}
2848 			}
2849 		);
2850 		+/
2851 
2852 		Widget previouslyFocusedWidget;
2853 
2854 		dropDown.visibilityChanged = (bool visible) {
2855 			if(visible) {
2856 				this.redraw();
2857 				captureMouse(this);
2858 				//dropDown.grabInput();
2859 
2860 				if(previouslyFocusedWidget is null)
2861 					previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget;
2862 				associatedWidget.parentWindow.focusedWidget = lw;
2863 			} else {
2864 				//dropDown.releaseInputGrab();
2865 				releaseMouseCapture();
2866 
2867 				if(!cancelled)
2868 					associatedWidget.setSelection(lw.getSelection);
2869 
2870 				associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget;
2871 			}
2872 		};
2873 
2874 		dropDown.show();
2875 	}
2876 
2877 	private bool shouldCloseIfClicked(Widget w) {
2878 		if(w is this)
2879 			return true;
2880 		version(custom_widgets)
2881 		if(cast(TextListViewWidget.TextListViewItem) w)
2882 			return true;
2883 		return false;
2884 	}
2885 
2886 	override void defaultEventHandler_click(ClickEvent ce) {
2887 		if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) {
2888 			this.win.close();
2889 		}
2890 	}
2891 
2892 	override void defaultEventHandler_char(CharEvent ce) {
2893 		if(ce.character == '\n')
2894 			this.win.close();
2895 	}
2896 
2897 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
2898 		if(kde.key == Key.Escape) {
2899 			cancelled = true;
2900 			this.win.close();
2901 		}/+ else if(kde.key == Key.Up || kde.key == Key.Down)
2902 			{} // intentionally blank, the list view handles these
2903 			// separately from the scroll message widget default handler
2904 		else if(lw && lw.glvw && lw.glvw.smw)
2905 			lw.glvw.smw.defaultKeyboardListener(kde);+/
2906 	}
2907 }
2908 
2909 /++
2910 	A drop-down list where the user must select one of the
2911 	given options. Like `<select>` in HTML.
2912 
2913 	The current selection is given as a string or an index.
2914 	It emits a SelectionChangedEvent when it changes.
2915 +/
2916 class DropDownSelection : ComboboxBase {
2917 	/++
2918 		Creates a drop down selection, optionally passing its initial list of options.
2919 
2920 		History:
2921 			The overload with the `options` parameter was added December 29, 2024.
2922 	+/
2923 	this(Widget parent) {
2924 		version(win32_widgets)
2925 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
2926 		else version(custom_widgets) {
2927 			super(parent);
2928 
2929 			addEventListener("focus", () { this.redraw; });
2930 			addEventListener("blur", () { this.redraw; });
2931 			addEventListener(EventType.change, () { this.redraw; });
2932 			addEventListener("mousedown", () { this.focus(); this.popup(); });
2933 			addEventListener((KeyDownEvent event) {
2934 				if(event.key == Key.Space)
2935 					popup();
2936 			});
2937 		} else static assert(false);
2938 	}
2939 
2940 	/// ditto
2941 	this(string[] options, Widget parent) {
2942 		this(parent);
2943 		this.options = options;
2944 	}
2945 
2946 	mixin Padding!q{2};
2947 	static class Style : Widget.Style {
2948 		override FrameStyle borderStyle() { return FrameStyle.risen; }
2949 	}
2950 	mixin OverrideStyle!Style;
2951 
2952 	version(custom_widgets)
2953 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2954 		auto cs = getComputedStyle();
2955 
2956 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
2957 
2958 		painter.outlineColor = cs.foregroundColor;
2959 		painter.fillColor = cs.foregroundColor;
2960 
2961 		/+
2962 		Point[4] triangle;
2963 		enum padding = 6;
2964 		enum paddingV = 7;
2965 		enum triangleWidth = 10;
2966 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
2967 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
2968 		triangle[2] = Point(width - padding - 0, paddingV);
2969 		triangle[3] = triangle[0];
2970 		painter.drawPolygon(triangle[]);
2971 		+/
2972 
2973 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
2974 
2975 		painter.drawPolygon(
2976 			scaleWithDpi(Point(2, 6) + offset),
2977 			scaleWithDpi(Point(7, 11) + offset),
2978 			scaleWithDpi(Point(12, 6) + offset),
2979 			scaleWithDpi(Point(2, 6) + offset)
2980 		);
2981 
2982 
2983 		return bounds;
2984 	}
2985 
2986 	version(win32_widgets)
2987 	override void registerMovement() {
2988 		version(win32_widgets) {
2989 			if(hwnd) {
2990 				auto pos = getChildPositionRelativeToParentHwnd(this);
2991 				// the height given to this from Windows' perspective is supposed
2992 				// to include the drop down's height. so I add to it to give some
2993 				// room for that.
2994 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
2995 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
2996 			}
2997 		}
2998 		sendResizeEvent();
2999 	}
3000 }
3001 
3002 /++
3003 	A text box with a drop down arrow listing selections.
3004 	The user can choose from the list, or type their own.
3005 +/
3006 class FreeEntrySelection : ComboboxBase {
3007 	this(Widget parent) {
3008 		this(null, parent);
3009 	}
3010 
3011 	this(string[] options, Widget parent) {
3012 		version(win32_widgets)
3013 			super(2 /* CBS_DROPDOWN */, parent);
3014 		else version(custom_widgets) {
3015 			super(parent);
3016 			auto hl = new HorizontalLayout(this);
3017 			lineEdit = new LineEdit(hl);
3018 
3019 			tabStop = false;
3020 
3021 			// lineEdit.addEventListener((FocusEvent fe) {  lineEdit.selectAll(); } );
3022 
3023 			auto btn = new class ArrowButton {
3024 				this() {
3025 					super(ArrowDirection.down, hl);
3026 				}
3027 				override int heightStretchiness() {
3028 					return 1;
3029 				}
3030 				override int heightShrinkiness() {
3031 					return 1;
3032 				}
3033 				override int maxHeight() {
3034 					return lineEdit.maxHeight;
3035 				}
3036 			};
3037 			//btn.addDirectEventListener("focus", &lineEdit.focus);
3038 			btn.addEventListener("triggered", &this.popup);
3039 			addEventListener(EventType.change, (Event event) {
3040 				lineEdit.content = event.stringValue;
3041 				lineEdit.focus();
3042 				redraw();
3043 			});
3044 		}
3045 		else static assert(false);
3046 
3047 		this.options = options;
3048 	}
3049 
3050 	string content() {
3051 		version(win32_widgets)
3052 			assert(0, "not implemented");
3053 		else version(custom_widgets)
3054 			return lineEdit.content;
3055 		else static assert(0);
3056 	}
3057 
3058 	void content(string s) {
3059 		version(win32_widgets)
3060 			assert(0, "not implemented");
3061 		else version(custom_widgets)
3062 			lineEdit.content = s;
3063 		else static assert(0);
3064 	}
3065 
3066 	version(custom_widgets) {
3067 		LineEdit lineEdit;
3068 
3069 		override int widthStretchiness() {
3070 			return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness;
3071 		}
3072 		override int flexBasisWidth() {
3073 			return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth;
3074 		}
3075 	}
3076 }
3077 
3078 /++
3079 	A combination of free entry with a list below it.
3080 +/
3081 class ComboBox : ComboboxBase {
3082 	this(Widget parent) {
3083 		version(win32_widgets)
3084 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
3085 		else version(custom_widgets) {
3086 			super(parent);
3087 			lineEdit = new LineEdit(this);
3088 			listWidget = new ListWidget(this);
3089 			listWidget.multiSelect = false;
3090 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
3091 				string c = null;
3092 				foreach(option; listWidget.options)
3093 					if(option.selected) {
3094 						c = option.label;
3095 						break;
3096 					}
3097 				lineEdit.content = c;
3098 			});
3099 
3100 			listWidget.tabStop = false;
3101 			this.tabStop = false;
3102 			listWidget.addEventListener("focusin", &lineEdit.focus);
3103 			this.addEventListener("focusin", &lineEdit.focus);
3104 
3105 			addDirectEventListener(EventType.change, {
3106 				listWidget.setSelection(selection_);
3107 				if(selection_ != -1)
3108 					lineEdit.content = options[selection_];
3109 				lineEdit.focus();
3110 				redraw();
3111 			});
3112 
3113 			lineEdit.addEventListener("focusin", &lineEdit.selectAll);
3114 
3115 			listWidget.addDirectEventListener(EventType.change, {
3116 				int set = -1;
3117 				foreach(idx, opt; listWidget.options)
3118 					if(opt.selected) {
3119 						set = cast(int) idx;
3120 						break;
3121 					}
3122 				if(set != selection_)
3123 					this.setSelection(set);
3124 			});
3125 		} else static assert(false);
3126 	}
3127 
3128 	override int minHeight() { return defaultLineHeight * 3; }
3129 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
3130 	override int heightStretchiness() { return 5; }
3131 
3132 	version(custom_widgets) {
3133 		LineEdit lineEdit;
3134 		ListWidget listWidget;
3135 
3136 		override void addOption(string s) {
3137 			listWidget.addOption(s);
3138 			ComboboxBase.addOption(s);
3139 		}
3140 
3141 		override void scrollSelectionIntoView() {
3142 			listWidget.scrollSelectionIntoView();
3143 		}
3144 	}
3145 }
3146 
3147 /+
3148 class Spinner : Widget {
3149 	version(win32_widgets)
3150 	this(Widget parent) {
3151 		super(parent);
3152 		parentWindow = parent.parentWindow;
3153 		auto hlayout = new HorizontalLayout(this);
3154 		lineEdit = new LineEdit(hlayout);
3155 		upDownControl = new UpDownControl(hlayout);
3156 	}
3157 
3158 	LineEdit lineEdit;
3159 	UpDownControl upDownControl;
3160 }
3161 
3162 class UpDownControl : Widget {
3163 	version(win32_widgets)
3164 	this(Widget parent) {
3165 		super(parent);
3166 		parentWindow = parent.parentWindow;
3167 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
3168 	}
3169 
3170 	override int minHeight() { return defaultLineHeight; }
3171 	override int maxHeight() { return defaultLineHeight * 3/2; }
3172 
3173 	override int minWidth() { return defaultLineHeight * 3/2; }
3174 	override int maxWidth() { return defaultLineHeight * 3/2; }
3175 }
3176 +/
3177 
3178 /+
3179 class DataView : Widget {
3180 	// this is the omnibus data viewer
3181 	// the internal data layout is something like:
3182 	// string[string][] but also each node can have parents
3183 }
3184 +/
3185 
3186 
3187 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
3188 
3189 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
3190 
3191 // FIXME: menus should prolly capture the mouse. ugh i kno.
3192 /*
3193 	TextEdit needs:
3194 
3195 	* caret manipulation
3196 	* selection control
3197 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
3198 
3199 	For example:
3200 
3201 	connect(paste, &textEdit.insertTextAtCaret);
3202 
3203 	would be nice.
3204 
3205 
3206 
3207 	I kinda want an omnibus dataview that combines list, tree,
3208 	and table - it can be switched dynamically between them.
3209 
3210 	Flattening policy: only show top level, show recursive, show grouped
3211 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
3212 
3213 	Single select, multi select, organization, drag+drop
3214 */
3215 
3216 //static if(UsingSimpledisplayX11)
3217 version(win32_widgets) {}
3218 else version(custom_widgets) {
3219 	enum scrollClickRepeatInterval = 50;
3220 
3221 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
3222 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
3223 	enum activeTabColor = lightAccentColor;
3224 	enum hoveringColor = Color(228, 228, 228);
3225 	enum buttonColor = windowBackgroundColor;
3226 	enum depressedButtonColor = darkAccentColor;
3227 	enum activeListXorColor = Color(255, 255, 127);
3228 	enum progressBarColor = Color(0, 0, 128);
3229 	enum activeMenuItemColor = Color(0, 0, 128);
3230 
3231 }}
3232 else static assert(false);
3233 deprecated("Get these properties off the `visualTheme` instead.") {
3234 	// these are used by horizontal rule so not just custom_widgets. for now at least.
3235 	enum darkAccentColor = Color(172, 172, 172);
3236 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
3237 }
3238 
3239 private const(wchar)* toWstringzInternal(in char[] s) {
3240 	wchar[] str;
3241 	str.reserve(s.length + 1);
3242 	foreach(dchar ch; s)
3243 		str ~= ch;
3244 	str ~= '\0';
3245 	return str.ptr;
3246 }
3247 
3248 static if(SimpledisplayTimerAvailable)
3249 void setClickRepeat(Widget w, int interval, int delay = 250) {
3250 	Timer timer;
3251 	int delayRemaining = delay / interval;
3252 	if(delayRemaining <= 1)
3253 		delayRemaining = 2;
3254 
3255 	immutable originalDelayRemaining = delayRemaining;
3256 
3257 	w.addDirectEventListener((scope MouseDownEvent ev) {
3258 		if(ev.srcElement !is w)
3259 			return;
3260 		if(timer !is null) {
3261 			timer.destroy();
3262 			timer = null;
3263 		}
3264 		delayRemaining = originalDelayRemaining;
3265 		timer = new Timer(interval, () {
3266 			if(delayRemaining > 0)
3267 				delayRemaining--;
3268 			else {
3269 				auto ev = new Event("triggered", w);
3270 				ev.sendDirectly();
3271 			}
3272 		});
3273 	});
3274 
3275 	w.addDirectEventListener((scope MouseUpEvent ev) {
3276 		if(ev.srcElement !is w)
3277 			return;
3278 		if(timer !is null) {
3279 			timer.destroy();
3280 			timer = null;
3281 		}
3282 	});
3283 
3284 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
3285 		if(ev.srcElement !is w)
3286 			return;
3287 		if(timer !is null) {
3288 			timer.destroy();
3289 			timer = null;
3290 		}
3291 	});
3292 
3293 }
3294 else
3295 void setClickRepeat(Widget w, int interval, int delay = 250) {}
3296 
3297 enum FrameStyle {
3298 	none, ///
3299 	risen, /// a 3d pop-out effect (think Windows 95 button)
3300 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
3301 	solid, ///
3302 	dotted, ///
3303 	fantasy, /// a style based on a popular fantasy video game
3304 	rounded, /// a rounded rectangle
3305 }
3306 
3307 version(custom_widgets)
3308 deprecated
3309 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
3310 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3311 }
3312 
3313 version(custom_widgets)
3314 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
3315 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
3316 }
3317 
3318 version(custom_widgets)
3319 deprecated
3320 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
3321 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3322 }
3323 
3324 int getBorderWidth(FrameStyle style) {
3325 	final switch(style) {
3326 		case FrameStyle.sunk, FrameStyle.risen:
3327 			return 2;
3328 		case FrameStyle.none:
3329 			return 0;
3330 		case FrameStyle.solid:
3331 			return 1;
3332 		case FrameStyle.dotted:
3333 			return 1;
3334 		case FrameStyle.fantasy:
3335 			return 3;
3336 		case FrameStyle.rounded:
3337 			return 2;
3338 	}
3339 }
3340 
3341 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
3342 	int borderWidth = getBorderWidth(style);
3343 	final switch(style) {
3344 		case FrameStyle.sunk, FrameStyle.risen:
3345 			// outer layer
3346 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
3347 		break;
3348 		case FrameStyle.none:
3349 			painter.outlineColor = background;
3350 		break;
3351 		case FrameStyle.solid:
3352 		case FrameStyle.rounded:
3353 			painter.pen = Pen(border, 1);
3354 		break;
3355 		case FrameStyle.dotted:
3356 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
3357 		break;
3358 		case FrameStyle.fantasy:
3359 			painter.pen = Pen(border, 3);
3360 		break;
3361 	}
3362 
3363 	painter.fillColor = background;
3364 
3365 	if(style == FrameStyle.rounded) {
3366 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
3367 	} else {
3368 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
3369 
3370 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
3371 			// 3d effect
3372 			auto vt = WidgetPainter.visualTheme;
3373 
3374 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
3375 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
3376 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
3377 
3378 			// inner layer
3379 			//right, bottom
3380 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
3381 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
3382 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
3383 			// left, top
3384 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
3385 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
3386 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
3387 		} else if(style == FrameStyle.fantasy) {
3388 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
3389 			painter.fillColor = Color.transparent;
3390 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
3391 		}
3392 	}
3393 
3394 	return borderWidth;
3395 }
3396 
3397 /++
3398 	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.
3399 
3400 	See_Also:
3401 		[MenuItem]
3402 		[ToolButton]
3403 		[Menu.addItem]
3404 +/
3405 class Action {
3406 	version(win32_widgets) {
3407 		private int id;
3408 		private static int lastId = 9000;
3409 		private static Action[int] mapping;
3410 	}
3411 
3412 	KeyEvent accelerator;
3413 
3414 	// FIXME: disable message
3415 	// and toggle thing?
3416 	// ??? and trigger arguments too ???
3417 
3418 	/++
3419 		Params:
3420 			label = the textual label
3421 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
3422 			triggered = initial handler, more can be added via the [triggered] member.
3423 	+/
3424 	this(string label, ushort icon = 0, void delegate() triggered = null) {
3425 		this.label = label;
3426 		this.iconId = icon;
3427 		if(triggered !is null)
3428 			this.triggered ~= triggered;
3429 		version(win32_widgets) {
3430 			id = ++lastId;
3431 			mapping[id] = this;
3432 		}
3433 	}
3434 
3435 	private string label;
3436 	private ushort iconId;
3437 	// icon
3438 
3439 	// when it is triggered, the triggered event is fired on the window
3440 	/// The list of handlers when it is triggered.
3441 	void delegate()[] triggered;
3442 }
3443 
3444 /*
3445 	plan:
3446 		keyboard accelerators
3447 
3448 		* menus (and popups and tooltips)
3449 		* status bar
3450 		* toolbars and buttons
3451 
3452 		sortable table view
3453 
3454 		maybe notification area icons
3455 		basic clipboard
3456 
3457 		* radio box
3458 		splitter
3459 		toggle buttons (optionally mutually exclusive, like in Paint)
3460 		label, rich text display, multi line plain text (selectable)
3461 		* fieldset
3462 		* nestable grid layout
3463 		single line text input
3464 		* multi line text input
3465 		slider
3466 		spinner
3467 		list box
3468 		drop down
3469 		combo box
3470 		auto complete box
3471 		* progress bar
3472 
3473 		terminal window/widget (on unix it might even be a pty but really idk)
3474 
3475 		ok button
3476 		cancel button
3477 
3478 		keyboard hotkeys
3479 
3480 		scroll widget
3481 
3482 		event redirections and network transparency
3483 		script integration
3484 */
3485 
3486 
3487 /*
3488 	MENUS
3489 
3490 	auto bar = new MenuBar(window);
3491 	window.menuBar = bar;
3492 
3493 	auto fileMenu = bar.addItem(new Menu("&File"));
3494 	fileMenu.addItem(new MenuItem("&Exit"));
3495 
3496 
3497 	EVENTS
3498 
3499 	For controls, you should usually use "triggered" rather than "click", etc., because
3500 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3501 	This is the case on menus and pushbuttons.
3502 
3503 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3504 */
3505 
3506 
3507 /*
3508 enum LinePreference {
3509 	AlwaysOnOwnLine, // always on its own line
3510 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3511 	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
3512 }
3513 */
3514 
3515 /++
3516 	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.
3517 
3518 	---
3519 	class MyWidget : Widget {
3520 		this(Widget parent) { super(parent); }
3521 
3522 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3523 		mixin Padding!q{4};
3524 
3525 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3526 		mixin Margin!q{8};
3527 
3528 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3529 		// while Top/Bottom/Right remain 8 from the mixin above.
3530 		override int marginLeft() { return 2; }
3531 	}
3532 	---
3533 
3534 
3535 	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]).
3536 
3537 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3538 
3539 	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!).
3540 
3541 	* 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.
3542 +/
3543 mixin template Padding(string code) {
3544 	override int paddingLeft() { return mixin(code);}
3545 	override int paddingRight() { return mixin(code);}
3546 	override int paddingTop() { return mixin(code);}
3547 	override int paddingBottom() { return mixin(code);}
3548 }
3549 
3550 /// ditto
3551 mixin template Margin(string code) {
3552 	override int marginLeft() { return mixin(code);}
3553 	override int marginRight() { return mixin(code);}
3554 	override int marginTop() { return mixin(code);}
3555 	override int marginBottom() { return mixin(code);}
3556 }
3557 
3558 private
3559 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3560 	enum calcingV = relevantMeasure == "height";
3561 
3562 	parent.registerMovement();
3563 
3564 	if(parent.children.length == 0)
3565 		return;
3566 
3567 	auto parentStyle = parent.getComputedStyle();
3568 
3569 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3570 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3571 
3572 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3573 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3574 
3575 	// my own width and height should already be set by the caller of this function...
3576 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3577 		mixin("parentStyle.padding"~firstThingy~"()") -
3578 		mixin("parentStyle.padding"~secondThingy~"()");
3579 
3580 	int stretchinessSum;
3581 	int stretchyChildSum;
3582 	int lastMargin = 0;
3583 
3584 	int shrinkinessSum;
3585 	int shrinkyChildSum;
3586 
3587 	// set initial size
3588 	foreach(child; parent.children) {
3589 
3590 		auto childStyle = child.getComputedStyle();
3591 
3592 		if(cast(StaticPosition) child)
3593 			continue;
3594 		if(child.hidden)
3595 			continue;
3596 
3597 		const iw = child.flexBasisWidth();
3598 		const ih = child.flexBasisHeight();
3599 
3600 		static if(calcingV) {
3601 			child.width = parent.width -
3602 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3603 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3604 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3605 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3606 
3607 			if(child.width < 0)
3608 				child.width = 0;
3609 			if(child.width > childStyle.maxWidth())
3610 				child.width = childStyle.maxWidth();
3611 
3612 			if(iw > 0) {
3613 				auto totalPossible = child.width;
3614 				if(child.width > iw && child.widthStretchiness() == 0)
3615 					child.width = iw;
3616 			}
3617 
3618 			child.height = mymax(childStyle.minHeight(), ih);
3619 		} else {
3620 			// set to take all the space
3621 			child.height = parent.height -
3622 				mixin("childStyle.margin"~firstThingy~"()") -
3623 				mixin("childStyle.margin"~secondThingy~"()") -
3624 				mixin("parentStyle.padding"~firstThingy~"()") -
3625 				mixin("parentStyle.padding"~secondThingy~"()");
3626 
3627 			// then clamp it
3628 			if(child.height < 0)
3629 				child.height = 0;
3630 			if(child.height > childStyle.maxHeight())
3631 				child.height = childStyle.maxHeight();
3632 
3633 			// and if possible, respect the ideal target
3634 			if(ih > 0) {
3635 				auto totalPossible = child.height;
3636 				if(child.height > ih && child.heightStretchiness() == 0)
3637 					child.height = ih;
3638 			}
3639 
3640 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3641 			child.width = mymax(childStyle.minWidth(), iw);
3642 		}
3643 
3644 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3645 
3646 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3647 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3648 		lastMargin = margin;
3649 		spaceRemaining -= thisMargin + margin;
3650 
3651 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3652 		stretchinessSum += s;
3653 		if(s > 0)
3654 			stretchyChildSum++;
3655 
3656 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3657 		shrinkinessSum += s2;
3658 		if(s2 > 0)
3659 			shrinkyChildSum++;
3660 	}
3661 
3662 	if(spaceRemaining < 0 && shrinkyChildSum) {
3663 		// shrink to get into the space if it is possible
3664 		auto toRemove = -spaceRemaining;
3665 		auto removalPerItem = toRemove / shrinkinessSum;
3666 		auto remainder = toRemove % shrinkinessSum;
3667 
3668 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3669 
3670 		foreach(child; parent.children) {
3671 			auto childStyle = child.getComputedStyle();
3672 			if(cast(StaticPosition) child)
3673 				continue;
3674 			if(child.hidden)
3675 				continue;
3676 			static if(calcingV) {
3677 				auto minimum = childStyle.minHeight();
3678 				auto stretch = childStyle.heightShrinkiness();
3679 			} else {
3680 				auto minimum = childStyle.minWidth();
3681 				auto stretch = childStyle.widthShrinkiness();
3682 			}
3683 
3684 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3685 				continue;
3686 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3687 
3688 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3689 
3690 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3691 		}
3692 	}
3693 
3694 	// stretch to fill space
3695 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3696 		auto spacePerChild = spaceRemaining / stretchinessSum;
3697 		bool spreadEvenly;
3698 		bool giveToBiggest;
3699 		if(spacePerChild <= 0) {
3700 			spacePerChild = spaceRemaining / stretchyChildSum;
3701 			spreadEvenly = true;
3702 		}
3703 		if(spacePerChild <= 0) {
3704 			giveToBiggest = true;
3705 		}
3706 		int previousSpaceRemaining = spaceRemaining;
3707 		stretchinessSum = 0;
3708 		Widget mostStretchy;
3709 		int mostStretchyS;
3710 		foreach(child; parent.children) {
3711 			auto childStyle = child.getComputedStyle();
3712 			if(cast(StaticPosition) child)
3713 				continue;
3714 			if(child.hidden)
3715 				continue;
3716 			static if(calcingV) {
3717 				auto maximum = childStyle.maxHeight();
3718 			} else {
3719 				auto maximum = childStyle.maxWidth();
3720 			}
3721 
3722 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3723 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3724 				mixin("child._" ~ relevantMeasure) -= adj;
3725 				spaceRemaining += adj;
3726 				continue;
3727 			}
3728 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3729 			if(s <= 0)
3730 				continue;
3731 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3732 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3733 			spaceRemaining -= spaceAdjustment;
3734 			if(mixin("child." ~ relevantMeasure) > maximum) {
3735 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3736 				mixin("child._" ~ relevantMeasure) -= diff;
3737 				spaceRemaining += diff;
3738 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3739 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3740 				if(mostStretchy is null || s >= mostStretchyS) {
3741 					mostStretchy = child;
3742 					mostStretchyS = s;
3743 				}
3744 			}
3745 		}
3746 
3747 		if(giveToBiggest && mostStretchy !is null) {
3748 			auto child = mostStretchy;
3749 			auto childStyle = child.getComputedStyle();
3750 			int spaceAdjustment = spaceRemaining;
3751 
3752 			static if(calcingV)
3753 				auto maximum = childStyle.maxHeight();
3754 			else
3755 				auto maximum = childStyle.maxWidth();
3756 
3757 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3758 			spaceRemaining -= spaceAdjustment;
3759 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3760 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3761 				mixin("child._" ~ relevantMeasure) -= diff;
3762 				spaceRemaining += diff;
3763 			}
3764 		}
3765 
3766 		if(spaceRemaining == previousSpaceRemaining) {
3767 			if(mostStretchy !is null) {
3768 				static if(calcingV)
3769 					auto maximum = mostStretchy.maxHeight();
3770 				else
3771 					auto maximum = mostStretchy.maxWidth();
3772 
3773 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3774 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3775 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3776 			}
3777 			break; // apparently nothing more we can do
3778 		}
3779 	}
3780 
3781 	foreach(child; parent.children) {
3782 		auto childStyle = child.getComputedStyle();
3783 		if(cast(StaticPosition) child)
3784 			continue;
3785 		if(child.hidden)
3786 			continue;
3787 
3788 		static if(calcingV)
3789 			auto maximum = childStyle.maxHeight();
3790 		else
3791 			auto maximum = childStyle.maxWidth();
3792 		if(mixin("child._" ~ relevantMeasure) > maximum)
3793 			mixin("child._" ~ relevantMeasure) = maximum;
3794 	}
3795 
3796 	// position
3797 	lastMargin = 0;
3798 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3799 	foreach(child; parent.children) {
3800 		auto childStyle = child.getComputedStyle();
3801 		if(cast(StaticPosition) child) {
3802 			child.recomputeChildLayout();
3803 			continue;
3804 		}
3805 		if(child.hidden)
3806 			continue;
3807 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3808 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3809 		currentPos += thisMargin;
3810 		static if(calcingV) {
3811 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3812 			child.y = currentPos;
3813 		} else {
3814 			child.x = currentPos;
3815 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3816 
3817 		}
3818 		currentPos += mixin("child." ~ relevantMeasure);
3819 		currentPos += margin;
3820 		lastMargin = margin;
3821 
3822 		child.recomputeChildLayout();
3823 	}
3824 }
3825 
3826 int mymax(int a, int b) { return a > b ? a : b; }
3827 int mymax(int a, int b, int c) {
3828 	auto d = mymax(a, b);
3829 	return c > d ? c : d;
3830 }
3831 
3832 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3833 // and here, it must be integrable with the layout, the event system, and not be painted over.
3834 version(win32_widgets) {
3835 
3836 	// this function just does stuff that a parent window needs for redirection
3837 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3838 		this_.hookedWndProc(msg, wParam, lParam);
3839 
3840 		switch(msg) {
3841 
3842 			case WM_VSCROLL, WM_HSCROLL:
3843 				auto pos = HIWORD(wParam);
3844 				auto m = LOWORD(wParam);
3845 
3846 				auto scrollbarHwnd = cast(HWND) lParam;
3847 
3848 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3849 
3850 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3851 
3852 					switch(m) {
3853 						/+
3854 						// I don't think those messages are ever actually sent normally by the widget itself,
3855 						// they are more used for the keyboard interface. methinks.
3856 						case SB_BOTTOM:
3857 							// writeln("end");
3858 							auto event = new Event("scrolltoend", *widgetp);
3859 							event.dispatch();
3860 							//if(!event.defaultPrevented)
3861 						break;
3862 						case SB_TOP:
3863 							// writeln("top");
3864 							auto event = new Event("scrolltobeginning", *widgetp);
3865 							event.dispatch();
3866 						break;
3867 						case SB_ENDSCROLL:
3868 							// idk
3869 						break;
3870 						+/
3871 						case SB_LINEDOWN:
3872 							(*widgetp).emitCommand!"scrolltonextline"();
3873 						return 0;
3874 						case SB_LINEUP:
3875 							(*widgetp).emitCommand!"scrolltopreviousline"();
3876 						return 0;
3877 						case SB_PAGEDOWN:
3878 							(*widgetp).emitCommand!"scrolltonextpage"();
3879 						return 0;
3880 						case SB_PAGEUP:
3881 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3882 						return 0;
3883 						case SB_THUMBPOSITION:
3884 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3885 							ev.dispatch();
3886 						return 0;
3887 						case SB_THUMBTRACK:
3888 							// eh kinda lying but i like the real time update display
3889 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3890 							ev.dispatch();
3891 
3892 							// the event loop doesn't seem to carry on with a requested redraw..
3893 							// so we request it to get our dirty bit set...
3894 							// then we need to immediately actually redraw it too for instant feedback to user
3895 							SimpleWindow.processAllCustomEvents();
3896 							SimpleWindow.processAllCustomEvents();
3897 							//if(this_.parentWindow)
3898 								//this_.parentWindow.actualRedraw();
3899 
3900 							// and this ensures the WM_PAINT message is sent fairly quickly
3901 							// still seems to lag a little in large windows but meh it basically works.
3902 							if(this_.parentWindow) {
3903 								// FIXME: if painting is slow, this does still lag
3904 								// we probably will want to expose some user hook to ScrollWindowEx
3905 								// or something.
3906 								UpdateWindow(this_.parentWindow.hwnd);
3907 							}
3908 						return 0;
3909 						default:
3910 					}
3911 				}
3912 			break;
3913 
3914 			case WM_CONTEXTMENU:
3915 				auto hwndFrom = cast(HWND) wParam;
3916 
3917 				auto xPos = cast(short) LOWORD(lParam);
3918 				auto yPos = cast(short) HIWORD(lParam);
3919 
3920 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3921 					POINT p;
3922 					p.x = xPos;
3923 					p.y = yPos;
3924 					ScreenToClient(hwnd, &p);
3925 					auto clientX = cast(ushort) p.x;
3926 					auto clientY = cast(ushort) p.y;
3927 
3928 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
3929 
3930 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
3931 						return 0;
3932 					}
3933 				}
3934 			break;
3935 
3936 			case WM_DRAWITEM:
3937 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
3938 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
3939 					return (*widgetp).handleWmDrawItem(dis);
3940 				}
3941 			break;
3942 
3943 			case WM_NOTIFY:
3944 				auto hdr = cast(NMHDR*) lParam;
3945 				auto hwndFrom = hdr.hwndFrom;
3946 				auto code = hdr.code;
3947 
3948 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
3949 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
3950 				}
3951 			break;
3952 			case WM_COMMAND:
3953 				auto handle = cast(HWND) lParam;
3954 				auto cmd = HIWORD(wParam);
3955 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
3956 
3957 			default:
3958 				// pass it on
3959 		}
3960 		return 0;
3961 	}
3962 
3963 
3964 
3965 	extern(Windows)
3966 	private
3967 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
3968 	// but can i merge them?!
3969 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
3970 		// try { writeln(iMessage); } catch(Exception e) {};
3971 
3972 		if(auto te = hWnd in Widget.nativeMapping) {
3973 			try {
3974 
3975 				te.hookedWndProc(iMessage, wParam, lParam);
3976 
3977 				int mustReturn;
3978 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
3979 				if(mustReturn)
3980 					return ret;
3981 
3982 				if(iMessage == WM_SETFOCUS) {
3983 					auto lol = *te;
3984 					while(lol !is null && lol.implicitlyCreated)
3985 						lol = lol.parent;
3986 					lol.focus();
3987 					//(*te).parentWindow.focusedWidget = lol;
3988 				}
3989 
3990 
3991 				if(iMessage == WM_CTLCOLOREDIT) {
3992 
3993 				}
3994 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
3995 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
3996 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
3997 						//GetStockObject(NULL_BRUSH);
3998 				}
3999 
4000 				auto pos = getChildPositionRelativeToParentOrigin(*te);
4001 				lastDefaultPrevented = false;
4002 				// try { writeln(typeid(*te)); } catch(Exception e) {}
4003 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
4004 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
4005 				else {
4006 					// it was something we recognized, should only call the window procedure if the default was not prevented
4007 				}
4008 			} catch(Exception e) {
4009 				assert(0, e.toString());
4010 			}
4011 			return 0;
4012 		}
4013 		assert(0, "shouldn't be receiving messages for this window....");
4014 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
4015 	}
4016 
4017 	extern(Windows)
4018 	private
4019 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
4020 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4021 		if(iMessage == WM_ERASEBKGND) {
4022 			auto dc = GetDC(hWnd);
4023 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
4024 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
4025 			RECT r;
4026 			GetWindowRect(hWnd, &r);
4027 			// since the pen is null, to fill the whole space, we need the +1 on both.
4028 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
4029 			SelectObject(dc, p);
4030 			SelectObject(dc, b);
4031 			ReleaseDC(hWnd, dc);
4032 			InvalidateRect(hWnd, null, false); // redraw the border
4033 			return 1;
4034 		}
4035 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
4036 	}
4037 
4038 	/++
4039 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
4040 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
4041 		of minigui's expectations.
4042 
4043 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
4044 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
4045 
4046 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
4047 
4048 		To check if you can use this, use `static if(UsingWin32Widgets)`.
4049 	+/
4050 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
4051 		assert(p.parentWindow !is null);
4052 		assert(p.parentWindow.win.impl.hwnd !is null);
4053 
4054 		auto bsgroupbox = style == BS_GROUPBOX;
4055 
4056 		HWND phwnd;
4057 
4058 		auto wtf = p.parent;
4059 		while(wtf) {
4060 			if(wtf.hwnd !is null) {
4061 				phwnd = wtf.hwnd;
4062 				break;
4063 			}
4064 			wtf = wtf.parent;
4065 		}
4066 
4067 		if(phwnd is null)
4068 			phwnd = p.parentWindow.win.impl.hwnd;
4069 
4070 		assert(phwnd !is null);
4071 
4072 		WCharzBuffer wt = WCharzBuffer(windowText);
4073 
4074 		style |= WS_VISIBLE | WS_CHILD;
4075 		//if(className != WC_TABCONTROL)
4076 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
4077 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
4078 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
4079 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
4080 
4081 		assert(p.hwnd !is null);
4082 
4083 
4084 		static HFONT font;
4085 		if(font is null) {
4086 			NONCLIENTMETRICS params;
4087 			params.cbSize = params.sizeof;
4088 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
4089 				font = CreateFontIndirect(&params.lfMessageFont);
4090 			}
4091 		}
4092 
4093 		if(font)
4094 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
4095 
4096 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
4097 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
4098 		Widget.nativeMapping[p.hwnd] = p;
4099 
4100 		if(bsgroupbox)
4101 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
4102 		else
4103 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4104 
4105 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
4106 
4107 		p.registerMovement();
4108 	}
4109 }
4110 
4111 version(win32_widgets)
4112 private
4113 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
4114 	if(hwnd is null || hwnd in Widget.nativeMapping)
4115 		return true;
4116 	auto parent = cast(Widget) cast(void*) lparam;
4117 	Widget p = new Widget(null);
4118 	p._parent = parent;
4119 	p.parentWindow = parent.parentWindow;
4120 	p.hwnd = hwnd;
4121 	p.implicitlyCreated = true;
4122 	Widget.nativeMapping[p.hwnd] = p;
4123 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4124 	return true;
4125 }
4126 
4127 /++
4128 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
4129 +/
4130 struct WidgetPainter {
4131 	this(ScreenPainter screenPainter, Widget drawingUpon) {
4132 		this.drawingUpon = drawingUpon;
4133 		this.screenPainter = screenPainter;
4134 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4135 			this.screenPainter.setFont(font);
4136 	}
4137 
4138 	/++
4139 		EXPERIMENTAL. subject to change.
4140 
4141 		When you draw a cursor, you can draw this to notify your window of where it is,
4142 		for IME systems to use.
4143 	+/
4144 	void notifyCursorPosition(int x, int y, int width, int height) {
4145 		if(auto a = drawingUpon.parentWindow)
4146 		if(auto w = a.inputProxy) {
4147 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
4148 		}
4149 	}
4150 
4151 
4152 	///
4153 	ScreenPainter screenPainter;
4154 	/// Forward to the screen painter for other methods
4155 	alias screenPainter this;
4156 
4157 	private Widget drawingUpon;
4158 
4159 	/++
4160 		This is the list of rectangles that actually need to be redrawn.
4161 
4162 		Not actually implemented yet.
4163 	+/
4164 	Rectangle[] invalidatedRectangles;
4165 
4166 	private static BaseVisualTheme _visualTheme;
4167 
4168 	/++
4169 		Functions to access the visual theme and helpers to easily use it.
4170 
4171 		These are aware of the current widget's computed style out of the theme.
4172 	+/
4173 	static @property BaseVisualTheme visualTheme() {
4174 		if(_visualTheme is null)
4175 			_visualTheme = new DefaultVisualTheme();
4176 		return _visualTheme;
4177 	}
4178 
4179 	/// ditto
4180 	static @property void visualTheme(BaseVisualTheme theme) {
4181 		_visualTheme = theme;
4182 
4183 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
4184 	}
4185 
4186 	/// ditto
4187 	Color themeForeground() {
4188 		return drawingUpon.getComputedStyle().foregroundColor();
4189 	}
4190 
4191 	/// ditto
4192 	Color themeBackground() {
4193 		return drawingUpon.getComputedStyle().background.color;
4194 	}
4195 
4196 	int isDarkTheme() {
4197 		return 0; // unspecified, yes, no as enum. FIXME
4198 	}
4199 
4200 	/++
4201 		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.
4202 
4203 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
4204 
4205 		If you change teh clip rectangle, you should change it back before you return.
4206 
4207 
4208 		The sequence it uses is:
4209 			background
4210 			content (delegated to you)
4211 			border
4212 			focused outline
4213 			selected overlay
4214 
4215 		Example code:
4216 
4217 		---
4218 		void paint(WidgetPainter painter) {
4219 			painter.drawThemed((bounds) {
4220 				return bounds; // if the selection overlay should be contained, you can return it here.
4221 			});
4222 		}
4223 		---
4224 	+/
4225 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
4226 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
4227 			return drawBody(bounds);
4228 		});
4229 	}
4230 	// this overload is actually mroe for setting the delegate to a virtual function
4231 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
4232 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
4233 
4234 		auto cs = drawingUpon.getComputedStyle();
4235 
4236 		auto bg = cs.background.color;
4237 
4238 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
4239 
4240 		rect.left += borderWidth;
4241 		rect.right -= borderWidth;
4242 		rect.top += borderWidth;
4243 		rect.bottom -= borderWidth;
4244 
4245 		auto insideBorderRect = rect;
4246 
4247 		rect.left += cs.paddingLeft;
4248 		rect.right -= cs.paddingRight;
4249 		rect.top += cs.paddingTop;
4250 		rect.bottom -= cs.paddingBottom;
4251 
4252 		this.outlineColor = this.themeForeground;
4253 		this.fillColor = bg;
4254 
4255 		auto widgetFont = cs.fontCached;
4256 		if(widgetFont !is null)
4257 			this.setFont(widgetFont);
4258 
4259 		rect = drawBody(this, rect);
4260 
4261 		if(widgetFont !is null) {
4262 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4263 				this.setFont(vtFont);
4264 			else
4265 				this.setFont(null);
4266 		}
4267 
4268 		if(auto os = cs.outlineStyle()) {
4269 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
4270 			this.fillColor = Color.transparent;
4271 			this.drawRectangle(insideBorderRect);
4272 		}
4273 	}
4274 
4275 	/++
4276 		First, draw the background.
4277 		Then draw your content.
4278 		Next, draw the border.
4279 		And the focused indicator.
4280 		And the is-selected box.
4281 
4282 		If it is focused i can draw the outline too...
4283 
4284 		If selected i can even do the xor action but that's at the end.
4285 	+/
4286 	void drawThemeBackground() {
4287 
4288 	}
4289 
4290 	void drawThemeBorder() {
4291 
4292 	}
4293 
4294 	// all this stuff is a dangerous experiment....
4295 	static class ScriptableVersion {
4296 		ScreenPainterImplementation* p;
4297 		int originX, originY;
4298 
4299 		@scriptable:
4300 		void drawRectangle(int x, int y, int width, int height) {
4301 			p.drawRectangle(x + originX, y + originY, width, height);
4302 		}
4303 		void drawLine(int x1, int y1, int x2, int y2) {
4304 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
4305 		}
4306 		void drawText(int x, int y, string text) {
4307 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
4308 		}
4309 		void setOutlineColor(int r, int g, int b) {
4310 			p.pen = Pen(Color(r,g,b), 1);
4311 		}
4312 		void setFillColor(int r, int g, int b) {
4313 			p.fillColor = Color(r,g,b);
4314 		}
4315 	}
4316 
4317 	ScriptableVersion toArsdJsvar() {
4318 		auto sv = new ScriptableVersion;
4319 		sv.p = this.screenPainter.impl;
4320 		sv.originX = this.screenPainter.originX;
4321 		sv.originY = this.screenPainter.originY;
4322 		return sv;
4323 	}
4324 
4325 	static WidgetPainter fromJsVar(T)(T t) {
4326 		return WidgetPainter.init;
4327 	}
4328 	// done..........
4329 }
4330 
4331 
4332 struct Style {
4333 	static struct helper(string m, T) {
4334 		enum method = m;
4335 		T v;
4336 
4337 		mixin template MethodOverride(typeof(this) v) {
4338 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
4339 		}
4340 	}
4341 
4342 	static auto opDispatch(string method, T)(T value) {
4343 		return helper!(method, T)(value);
4344 	}
4345 }
4346 
4347 /++
4348 	Implementation detail of the [ControlledBy] UDA.
4349 
4350 	History:
4351 		Added Oct 28, 2020
4352 +/
4353 struct ControlledBy_(T, Args...) {
4354 	Args args;
4355 
4356 	static if(Args.length)
4357 	this(Args args) {
4358 		this.args = args;
4359 	}
4360 
4361 	private T construct(Widget parent) {
4362 		return new T(args, parent);
4363 	}
4364 }
4365 
4366 /++
4367 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
4368 
4369 	History:
4370 		Added Oct 28, 2020
4371 +/
4372 auto ControlledBy(T, Args...)(Args args) {
4373 	return ControlledBy_!(T, Args)(args);
4374 }
4375 
4376 struct ContainerMeta {
4377 	string name;
4378 	ContainerMeta[] children;
4379 	Widget function(Widget parent) factory;
4380 
4381 	Widget instantiate(Widget parent) {
4382 		auto n = factory(parent);
4383 		n.name = name;
4384 		foreach(child; children)
4385 			child.instantiate(n);
4386 		return n;
4387 	}
4388 }
4389 
4390 /++
4391 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
4392 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
4393 
4394 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
4395 	structures. It works fine on structs declared inside functions though.
4396 
4397 	See: https://issues.dlang.org/show_bug.cgi?id=21984
4398 +/
4399 template Container(CArgs...) {
4400 	static if(CArgs.length && is(CArgs[0] : Widget)) {
4401 		private alias Super = CArgs[0];
4402 		private alias CArgs2 = CArgs[1 .. $];
4403 	} else {
4404 		private alias Super = Layout;
4405 		private alias CArgs2 = CArgs;
4406 	}
4407 
4408 	class Container : Super {
4409 		this(Widget parent) { super(parent); }
4410 
4411 		// just to partially support old gdc versions
4412 		version(GNU) {
4413 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
4414 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
4415 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
4416 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
4417 		} else mixin(q{
4418 			static foreach(Arg; CArgs2) {
4419 				mixin Arg.MethodOverride!(Arg);
4420 			}
4421 		});
4422 
4423 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
4424 			return ContainerMeta(
4425 				name,
4426 				children.dup,
4427 				function (Widget parent) { return new typeof(this)(parent); }
4428 			);
4429 		}
4430 
4431 		static ContainerMeta opCall(ContainerMeta[] children...) {
4432 			return opCall(null, children);
4433 		}
4434 	}
4435 }
4436 
4437 /++
4438 	The data controller widget is created by reflecting over the given
4439 	data type. You can use [ControlledBy] as a UDA on a struct or
4440 	just let it create things automatically.
4441 
4442 	Unlike [dialog], this uses real-time updating of the data and
4443 	you add it to another window yourself.
4444 
4445 	---
4446 		struct Test {
4447 			int x;
4448 			int y;
4449 		}
4450 
4451 		auto window = new Window();
4452 		auto dcw = new DataControllerWidget!Test(new Test, window);
4453 	---
4454 
4455 	The way it works is any public members are given a widget based
4456 	on their data type, and public methods trigger an action button
4457 	if no relevant parameters or a dialog action if it does have
4458 	parameters, similar to the [menu] facility.
4459 
4460 	If you change data programmatically, without going through the
4461 	DataControllerWidget methods, you will have to tell it something
4462 	has changed and it needs to redraw. This is done with the `invalidate`
4463 	method.
4464 
4465 	History:
4466 		Added Oct 28, 2020
4467 +/
4468 /// Group: generating_from_code
4469 class DataControllerWidget(T) : WidgetContainer {
4470 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4471 		private alias Tref = T;
4472 	else
4473 		private alias Tref = T*;
4474 
4475 	Tref datum;
4476 
4477 	/++
4478 		See_also: [addDataControllerWidget]
4479 	+/
4480 	this(Tref datum, Widget parent) {
4481 		this.datum = datum;
4482 
4483 		Widget cp = this;
4484 
4485 		super(parent);
4486 
4487 		foreach(attr; __traits(getAttributes, T))
4488 			static if(is(typeof(attr) == ContainerMeta)) {
4489 				cp = attr.instantiate(this);
4490 			}
4491 
4492 		auto def = this.getByName("default");
4493 		if(def !is null)
4494 			cp = def;
4495 
4496 		Widget helper(string name) {
4497 			auto maybe = this.getByName(name);
4498 			if(maybe is null)
4499 				return cp;
4500 			return maybe;
4501 
4502 		}
4503 
4504 		foreach(member; __traits(allMembers, T))
4505 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4506 		static if(is(typeof(__traits(getMember, this.datum, member))))
4507 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4508 			void delegate() update;
4509 
4510 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4511 
4512 			if(update)
4513 				updaters ~= update;
4514 
4515 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4516 				w.addEventListener("triggered", delegate() {
4517 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4518 					notifyDataUpdated();
4519 				});
4520 			} else static if(is(typeof(w.isChecked) == bool)) {
4521 				w.addEventListener(EventType.change, (Event ev) {
4522 					__traits(getMember, this.datum, member) = w.isChecked;
4523 				});
4524 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4525 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4526 			} else static if(is(typeof(w.value) == int)) {
4527 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4528 			} else static if(is(typeof(w) == DropDownSelection)) {
4529 				// special case for this to kinda support enums and such. coudl be better though
4530 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4531 			} else {
4532 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4533 			}
4534 		}
4535 	}
4536 
4537 	/++
4538 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4539 
4540 		History:
4541 			Added May 28, 2021
4542 	+/
4543 	void notifyDataUpdated() {
4544 		foreach(updater; updaters)
4545 			updater();
4546 
4547 		this.emit!(ChangeEvent!void)(delegate{});
4548 	}
4549 
4550 	private Widget[string] memberWidgets;
4551 	private void delegate()[] updaters;
4552 
4553 	mixin Emits!(ChangeEvent!void);
4554 }
4555 
4556 private int saturatedSum(int[] values...) {
4557 	int sum;
4558 	foreach(value; values) {
4559 		if(value == int.max)
4560 			return int.max;
4561 		sum += value;
4562 	}
4563 	return sum;
4564 }
4565 
4566 void genericSetValue(T, W)(T* where, W what) {
4567 	import std.conv;
4568 	*where = to!T(what);
4569 	//*where = cast(T) stringToLong(what);
4570 }
4571 
4572 /++
4573 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4574 
4575 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4576 
4577 	Note that this creates the widget but does not attach any event handlers to it.
4578 +/
4579 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4580 
4581 	string displayName = __traits(identifier, tt).beautify;
4582 
4583 	static if(controlledByCount!tt == 1) {
4584 		foreach(i, attr; __traits(getAttributes, tt)) {
4585 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4586 				auto w = attr.construct(parent);
4587 				static if(__traits(compiles, w.setPosition(*valptr)))
4588 					update = () { w.setPosition(*valptr); };
4589 				else static if(__traits(compiles, w.setValue(*valptr)))
4590 					update = () { w.setValue(*valptr); };
4591 
4592 				if(update)
4593 					update();
4594 				return w;
4595 			}
4596 		}
4597 	} else static if(controlledByCount!tt == 0) {
4598 		static if(is(typeof(tt) == enum)) {
4599 			// FIXME: update
4600 			auto dds = new DropDownSelection(parent);
4601 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4602 				dds.addOption(option);
4603 				if(__traits(getMember, typeof(tt), option) == *valptr)
4604 					dds.setSelection(cast(int) idx);
4605 			}
4606 			return dds;
4607 		} else static if(is(typeof(tt) == bool)) {
4608 			auto box = new Checkbox(displayName, parent);
4609 			update = () { box.isChecked = *valptr; };
4610 			update();
4611 			return box;
4612 		} else static if(is(typeof(tt) : const long)) {
4613 			auto le = new LabeledLineEdit(displayName, parent);
4614 			update = () { le.content = toInternal!string(*valptr); };
4615 			update();
4616 			return le;
4617 		} else static if(is(typeof(tt) : const double)) {
4618 			auto le = new LabeledLineEdit(displayName, parent);
4619 			import std.conv;
4620 			update = () { le.content = to!string(*valptr); };
4621 			update();
4622 			return le;
4623 		} else static if(is(typeof(tt) : const string)) {
4624 			auto le = new LabeledLineEdit(displayName, parent);
4625 			update = () { le.content = *valptr; };
4626 			update();
4627 			return le;
4628 		} else static if(is(typeof(tt) == function)) {
4629 			auto w = new Button(displayName, parent);
4630 			return w;
4631 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4632 			return parent.addDataControllerWidget(tt);
4633 		} else static assert(0, typeof(tt).stringof);
4634 	} else static assert(0, "multiple controllers not yet supported");
4635 }
4636 
4637 private template controlledByCount(alias tt) {
4638 	static int helper() {
4639 		int count;
4640 		foreach(i, attr; __traits(getAttributes, tt))
4641 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4642 				count++;
4643 		return count;
4644 	}
4645 
4646 	enum controlledByCount = helper;
4647 }
4648 
4649 /++
4650 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4651 
4652 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4653 
4654 	History:
4655 		The `redrawOnChange` parameter was added on May 28, 2021.
4656 +/
4657 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4658 	auto dcw = new DataControllerWidget!T(t, parent);
4659 	initializeDataControllerWidget(dcw, redrawOnChange);
4660 	return dcw;
4661 }
4662 
4663 /// ditto
4664 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4665 	auto dcw = new DataControllerWidget!T(t, parent);
4666 	initializeDataControllerWidget(dcw, redrawOnChange);
4667 	return dcw;
4668 }
4669 
4670 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4671 	if(redrawOnChange !is null)
4672 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4673 }
4674 
4675 /++
4676 	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.
4677 
4678 	History:
4679 		Finalized on June 3, 2021 for the dub v10.0 release
4680 +/
4681 struct StyleInformation {
4682 	private Widget w;
4683 	private BaseVisualTheme visualTheme;
4684 
4685 	private this(Widget w) {
4686 		this.w = w;
4687 		this.visualTheme = WidgetPainter.visualTheme;
4688 	}
4689 
4690 	/++
4691 		Forwards to [Widget.Style]
4692 
4693 		Bugs:
4694 			It is supposed to fall back to the [VisualTheme] if
4695 			the style doesn't override the default, but that is
4696 			not generally implemented. Many of them may end up
4697 			being explicit overloads instead of the generic
4698 			opDispatch fallback, like [font] is now.
4699 	+/
4700 	public @property opDispatch(string name)() {
4701 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4702 		w.useStyleProperties((scope Widget.Style props) {
4703 		//visualTheme.useStyleProperties(w, (props) {
4704 			prop = __traits(getMember, props, name);
4705 		});
4706 		return prop;
4707 	}
4708 
4709 	/++
4710 		Returns the cached font object associated with the widget,
4711 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4712 
4713 		History:
4714 			Prior to March 21, 2022 (dub v10.7), `font` went through
4715 			[opDispatch], which did not use the cache. You can now call it
4716 			repeatedly without guilt.
4717 	+/
4718 	public @property OperatingSystemFont font() {
4719 		OperatingSystemFont prop;
4720 		w.useStyleProperties((scope Widget.Style props) {
4721 			prop = props.fontCached;
4722 		});
4723 		if(prop is null) {
4724 			prop = visualTheme.defaultFontCached(w.currentDpi);
4725 		}
4726 		return prop;
4727 	}
4728 
4729 	@property {
4730 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4731 		/** */ int paddingLeft() { return w.paddingLeft(); }
4732 		/** */ int paddingRight() { return w.paddingRight(); }
4733 		/** */ int paddingTop() { return w.paddingTop(); }
4734 		/** */ int paddingBottom() { return w.paddingBottom(); }
4735 
4736 		/** */ int marginLeft() { return w.marginLeft(); }
4737 		/** */ int marginRight() { return w.marginRight(); }
4738 		/** */ int marginTop() { return w.marginTop(); }
4739 		/** */ int marginBottom() { return w.marginBottom(); }
4740 
4741 		/** */ int maxHeight() { return w.maxHeight(); }
4742 		/** */ int minHeight() { return w.minHeight(); }
4743 
4744 		/** */ int maxWidth() { return w.maxWidth(); }
4745 		/** */ int minWidth() { return w.minWidth(); }
4746 
4747 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4748 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4749 
4750 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4751 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4752 
4753 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4754 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4755 
4756 		// Global helpers some of these are unstable.
4757 		static:
4758 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4759 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4760 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4761 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4762 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4763 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4764 
4765 		/** */ Color activeTabColor() { return lightAccentColor; }
4766 		/** */ Color buttonColor() { return windowBackgroundColor; }
4767 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4768 		/** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; }
4769 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4770 			auto c = WidgetPainter.visualTheme.selectionColor();
4771 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4772 		}
4773 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4774 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4775 	}
4776 
4777 
4778 
4779 	/+
4780 
4781 	private static auto extractStyleProperty(string name)(Widget w) {
4782 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4783 		w.useStyleProperties((props) {
4784 			prop = __traits(getMember, props, name);
4785 		});
4786 		return prop;
4787 	}
4788 
4789 	// FIXME: clear this upon a X server disconnect
4790 	private static OperatingSystemFont[string] fontCache;
4791 
4792 	T getProperty(T)(string name, lazy T default_) {
4793 		if(visualTheme !is null) {
4794 			auto str = visualTheme.getPropertyString(w, name);
4795 			if(str is null)
4796 				return default_;
4797 			static if(is(T == Color))
4798 				return Color.fromString(str);
4799 			else static if(is(T == Measurement))
4800 				return Measurement(cast(int) toInternal!int(str));
4801 			else static if(is(T == WidgetBackground))
4802 				return WidgetBackground.fromString(str);
4803 			else static if(is(T == OperatingSystemFont)) {
4804 				if(auto f = str in fontCache)
4805 					return *f;
4806 				else
4807 					return fontCache[str] = new OperatingSystemFont(str);
4808 			} else static if(is(T == FrameStyle)) {
4809 				switch(str) {
4810 					default:
4811 						return FrameStyle.none;
4812 					foreach(style; __traits(allMembers, FrameStyle))
4813 					case style:
4814 						return __traits(getMember, FrameStyle, style);
4815 				}
4816 			} else static assert(0);
4817 		} else
4818 			return default_;
4819 	}
4820 
4821 	static struct Measurement {
4822 		int value;
4823 		alias value this;
4824 	}
4825 
4826 	@property:
4827 
4828 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4829 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4830 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4831 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4832 
4833 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4834 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4835 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4836 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4837 
4838 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4839 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4840 
4841 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4842 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4843 
4844 
4845 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4846 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4847 
4848 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4849 
4850 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4851 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4852 
4853 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4854 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4855 
4856 
4857 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4858 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4859 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4860 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4861 
4862 	Color activeTabColor() { return lightAccentColor; }
4863 	Color buttonColor() { return windowBackgroundColor; }
4864 	Color depressedButtonColor() { return darkAccentColor; }
4865 	Color hoveringColor() { return Color(228, 228, 228); }
4866 	Color activeListXorColor() {
4867 		auto c = WidgetPainter.visualTheme.selectionColor();
4868 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4869 	}
4870 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4871 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4872 	+/
4873 }
4874 
4875 
4876 
4877 // pragma(msg, __traits(classInstanceSize, Widget));
4878 
4879 /*private*/ template EventString(E) {
4880 	static if(is(typeof(E.EventString)))
4881 		enum EventString = E.EventString;
4882 	else
4883 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4884 }
4885 
4886 /*private*/ template EventStringIdentifier(E) {
4887 	string helper() {
4888 		auto es = EventString!E;
4889 		char[] id = new char[](es.length * 2);
4890 		size_t idx;
4891 		foreach(char ch; es) {
4892 			id[idx++] = cast(char)('a' + (ch >> 4));
4893 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4894 		}
4895 		return cast(string) id;
4896 	}
4897 
4898 	enum EventStringIdentifier = helper();
4899 }
4900 
4901 
4902 template classStaticallyEmits(This, EventType) {
4903 	static if(is(This Base == super))
4904 		static if(is(Base : Widget))
4905 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4906 		else
4907 			enum baseEmits = false;
4908 	else
4909 		enum baseEmits = false;
4910 
4911 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
4912 
4913 	enum classStaticallyEmits = thisEmits || baseEmits;
4914 }
4915 
4916 /++
4917 	A helper to make widgets out of other native windows.
4918 
4919 	History:
4920 		Factored out of OpenGlWidget on November 5, 2021
4921 +/
4922 class NestedChildWindowWidget : Widget {
4923 	SimpleWindow win;
4924 
4925 	/++
4926 		Used on X to send focus to the appropriate child window when requested by the window manager.
4927 
4928 		Normally returns its own nested window. Can also return another child or null to revert to the parent
4929 		if you override it in a child class.
4930 
4931 		History:
4932 			Added April 2, 2022 (dub v10.8)
4933 	+/
4934 	SimpleWindow focusableWindow() {
4935 		return win;
4936 	}
4937 
4938 	///
4939 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
4940 	this(SimpleWindow win, Widget parent) {
4941 		this.parentWindow = parent.parentWindow;
4942 		this.win = win;
4943 
4944 		super(parent);
4945 		windowsetup(win);
4946 	}
4947 
4948 	static protected SimpleWindow getParentWindow(Widget parent) {
4949 		assert(parent !is null);
4950 		SimpleWindow pwin = parent.parentWindow.win;
4951 
4952 		version(win32_widgets) {
4953 			HWND phwnd;
4954 			auto wtf = parent;
4955 			while(wtf) {
4956 				if(wtf.hwnd) {
4957 					phwnd = wtf.hwnd;
4958 					break;
4959 				}
4960 				wtf = wtf.parent;
4961 			}
4962 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
4963 			if(phwnd)
4964 				pwin = new SimpleWindow(phwnd);
4965 		}
4966 
4967 		return pwin;
4968 	}
4969 
4970 	/++
4971 		Called upon the nested window being destroyed.
4972 		Remember the window has already been destroyed at
4973 		this point, so don't use the native handle for anything.
4974 
4975 		History:
4976 			Added April 3, 2022 (dub v10.8)
4977 	+/
4978 	protected void dispose() {
4979 
4980 	}
4981 
4982 	protected void windowsetup(SimpleWindow w) {
4983 		/*
4984 		win.onFocusChange = (bool getting) {
4985 			if(getting)
4986 				this.focus();
4987 		};
4988 		*/
4989 
4990 		/+
4991 		win.onFocusChange = (bool getting) {
4992 			if(getting) {
4993 				this.parentWindow.focusedWidget = this;
4994 				this.emit!FocusEvent();
4995 				this.emit!FocusInEvent();
4996 			} else {
4997 				this.emit!BlurEvent();
4998 				this.emit!FocusOutEvent();
4999 			}
5000 		};
5001 		+/
5002 
5003 		win.onDestroyed = () {
5004 			this.dispose();
5005 		};
5006 
5007 		version(win32_widgets) {
5008 			Widget.nativeMapping[win.hwnd] = this;
5009 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
5010 		} else {
5011 			win.setEventHandlers(
5012 				(MouseEvent e) {
5013 					Widget p = this;
5014 					while(p ! is parentWindow) {
5015 						e.x += p.x;
5016 						e.y += p.y;
5017 						p = p.parent;
5018 					}
5019 					parentWindow.dispatchMouseEvent(e);
5020 				},
5021 				(KeyEvent e) {
5022 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
5023 					parentWindow.dispatchKeyEvent(e);
5024 				},
5025 				(dchar e) {
5026 					parentWindow.dispatchCharEvent(e);
5027 				},
5028 			);
5029 		}
5030 
5031 	}
5032 
5033 	override bool showOrHideIfNativeWindow(bool shouldShow) {
5034 		auto cur = hidden;
5035 		win.hidden = !shouldShow;
5036 		if(cur != shouldShow && shouldShow)
5037 			redraw();
5038 		return true;
5039 	}
5040 
5041 	/// OpenGL widgets cannot have child widgets. Do not call this.
5042 	/* @disable */ final override void addChild(Widget, int) {
5043 		throw new Error("cannot add children to OpenGL widgets");
5044 	}
5045 
5046 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
5047 	/// Keep in mind that events like mouse coordinates are still relative to your size.
5048 	override void registerMovement() {
5049 		// writefln("%d %d %d %d", x,y,width,height);
5050 		version(win32_widgets)
5051 			auto pos = getChildPositionRelativeToParentHwnd(this);
5052 		else
5053 			auto pos = getChildPositionRelativeToParentOrigin(this);
5054 		win.moveResize(pos[0], pos[1], width, height);
5055 
5056 		registerMovementAdditionalWork();
5057 		sendResizeEvent();
5058 	}
5059 
5060 	abstract void registerMovementAdditionalWork();
5061 }
5062 
5063 /++
5064 	Nests an opengl capable window inside this window as a widget.
5065 
5066 	You may also just want to create an additional [SimpleWindow] with
5067 	[OpenGlOptions.yes] yourself.
5068 
5069 	An OpenGL widget cannot have child widgets. It will throw if you try.
5070 +/
5071 static if(OpenGlEnabled)
5072 class OpenGlWidget : NestedChildWindowWidget {
5073 
5074 	override void registerMovementAdditionalWork() {
5075 		win.setAsCurrentOpenGlContext();
5076 	}
5077 
5078 	///
5079 	this(Widget parent) {
5080 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5081 		super(win, parent);
5082 	}
5083 
5084 	override void paint(WidgetPainter painter) {
5085 		win.setAsCurrentOpenGlContext();
5086 		glViewport(0, 0, this.width, this.height);
5087 		win.redrawOpenGlSceneNow();
5088 	}
5089 
5090 	void redrawOpenGlScene(void delegate() dg) {
5091 		win.redrawOpenGlScene = dg;
5092 	}
5093 }
5094 
5095 /++
5096 	This demo shows how to draw text in an opengl scene.
5097 +/
5098 unittest {
5099 	import arsd.minigui;
5100 	import arsd.ttf;
5101 
5102 	void main() {
5103 		auto window = new Window();
5104 
5105 		auto widget = new OpenGlWidget(window);
5106 
5107 		// old means non-shader code so compatible with glBegin etc.
5108 		// tbh I haven't implemented new one in font yet...
5109 		// anyway, declaring here, will construct soon.
5110 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
5111 
5112 		// this is a little bit awkward, calling some methods through
5113 		// the underlying SimpleWindow `win` method, and you can't do this
5114 		// on a nanovega widget due to conflicts so I should probably fix
5115 		// the api to be a bit easier. But here it will work.
5116 		//
5117 		// Alternatively, you could load the font on the first draw, inside
5118 		// the redrawOpenGlScene, and keep a flag so you don't do it every
5119 		// time. That'd be a bit easier since the lib sets up the context
5120 		// by then guaranteed.
5121 		//
5122 		// But still, I wanna show this.
5123 		widget.win.visibleForTheFirstTime = delegate {
5124 			// must set the opengl context
5125 			widget.win.setAsCurrentOpenGlContext();
5126 
5127 			// if you were doing a OpenGL 3+ shader, this
5128 			// gets especially important to do in order. With
5129 			// old-style opengl, I think you can even do it
5130 			// in main(), but meh, let's show it more correctly.
5131 
5132 			// Anyway, now it is time to load the font from the
5133 			// OS (you can alternatively load one from a .ttf file
5134 			// you bundle with the application), then load the
5135 			// font into texture for drawing.
5136 
5137 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
5138 
5139 			assert(!osfont.isNull()); // make sure it actually loaded
5140 
5141 			// using typeof to avoid repeating the long name lol
5142 			glfont = new typeof(glfont)(
5143 				// get the raw data from the font for loading in here
5144 				// since it doesn't use the OS function to draw the
5145 				// text, we gotta treat it more as a file than as
5146 				// a drawing api.
5147 				osfont.getTtfBytes(),
5148 				18, // need to respecify size since opengl world is different coordinate system
5149 
5150 				// these last two numbers are why it is called
5151 				// "Limited" font. It only loads the characters
5152 				// in the given range, since the texture atlas
5153 				// it references is all a big image generated ahead
5154 				// of time. You could maybe do the whole thing but
5155 				// idk how much memory that is.
5156 				//
5157 				// But here, 0-128 represents the ASCII range, so
5158 				// good enough for most English things, numeric labels,
5159 				// etc.
5160 				0,
5161 				128
5162 			);
5163 		};
5164 
5165 		widget.redrawOpenGlScene = () {
5166 			// now we can use the glfont's drawString function
5167 
5168 			// first some opengl setup. You can do this in one place
5169 			// on window first visible too in many cases, just showing
5170 			// here cuz it is easier for me.
5171 
5172 			// gonna need some alpha blending or it just looks awful
5173 			glEnable(GL_BLEND);
5174 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
5175 			glClearColor(0,0,0,0);
5176 			glDepthFunc(GL_LEQUAL);
5177 
5178 			// Also need to enable 2d textures, since it draws the
5179 			// font characters as images baked in
5180 			glMatrixMode(GL_MODELVIEW);
5181 			glLoadIdentity();
5182 			glDisable(GL_DEPTH_TEST);
5183 			glEnable(GL_TEXTURE_2D);
5184 
5185 			// the orthographic matrix is best for 2d things like text
5186 			// so let's set that up. This matrix makes the coordinates
5187 			// in the opengl scene be one-to-one with the actual pixels
5188 			// on screen. (Not necessarily best, you may wish to scale
5189 			// things, but it does help keep fonts looking normal.)
5190 			glMatrixMode(GL_PROJECTION);
5191 			glLoadIdentity();
5192 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
5193 
5194 			// you can do other glScale, glRotate, glTranslate, etc
5195 			// to the matrix here of course if you want.
5196 
5197 			// note the x,y coordinates here are for the text baseline
5198 			// NOT the upper-left corner. The baseline is like the line
5199 			// in the notebook you write on. Most the letters are actually
5200 			// above it, but some, like p and q, dip a bit below it.
5201 			//
5202 			// So if you're used to the upper left coordinate like the
5203 			// rest of simpledisplay/minigui usually do, do the
5204 			// y + glfont.ascent to bring it down a little. So this
5205 			// example puts the string in the upper left of the window.
5206 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
5207 
5208 			// re color btw: the function sets a solid color internally,
5209 			// but you actually COULD do your own thing for rainbow effects
5210 			// and the sort if you wanted too, by pulling its guts out.
5211 			// Just view its source for an idea of how it actually draws:
5212 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
5213 
5214 			// it gets a bit complicated with the character positioning,
5215 			// but the opengl parts are fairly simple: bind a texture,
5216 			// set the color, draw a quad for each letter.
5217 
5218 
5219 			// the last optional argument there btw is a bounding box
5220 			// it will/ use to word wrap and return an object you can
5221 			// use to implement scrolling or pagination; it tells how
5222 			// much of the string didn't fit in the box. But for simple
5223 			// labels we can just ignore that.
5224 
5225 
5226 			// I'd suggest drawing text as the last step, after you
5227 			// do your other drawing. You might use the push/pop matrix
5228 			// stuff to keep your place. You, in theory, should be able
5229 			// to do text in a 3d space but I've never actually tried
5230 			// that....
5231 		};
5232 
5233 		window.loop();
5234 	}
5235 }
5236 
5237 version(custom_widgets)
5238 private class TextListViewWidget : GenericListViewWidget {
5239 	static class TextListViewItem : GenericListViewItem {
5240 		ListWidget controller;
5241 		this(ListWidget controller, Widget parent) {
5242 			this.controller = controller;
5243 			this.tabStop = false;
5244 			super(parent);
5245 		}
5246 
5247 		ListWidget.Option* showing;
5248 
5249 		override void showItem(int idx) {
5250 			showing = idx < controller.options.length ? &controller.options[idx] : null;
5251 			redraw(); // is this necessary? the generic thing might call it...
5252 		}
5253 
5254 		override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
5255 			if(showing is null)
5256 				return bounds;
5257 			painter.drawText(bounds.upperLeft, showing.label);
5258 			return bounds;
5259 		}
5260 
5261 		static class Style : Widget.Style {
5262 			override WidgetBackground background() {
5263 				// FIXME: change it if it is focused or not
5264 				// needs to reliably detect if focused (noting the actual focus may be on a parent or child... or even sibling for FreeEntrySelection. maybe i just need a better way to proxy focus in widgets generically). also will need to redraw correctly without defaultEventHandler_focusin hacks like EditableTextWidget uses
5265 				auto tlvi = cast(TextListViewItem) widget;
5266 				if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected)
5267 					return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode
5268 				return super.background();
5269 			}
5270 
5271 			override Color foregroundColor() {
5272 				auto tlvi = cast(TextListViewItem) widget;
5273 				return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor();
5274 			}
5275 
5276 			override FrameStyle outlineStyle() {
5277 				// FIXME: change it if it is focused or not
5278 				auto tlvi = cast(TextListViewItem) widget;
5279 				return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle();
5280 			}
5281 		}
5282 		mixin OverrideStyle!Style;
5283 
5284 		mixin Padding!q{2};
5285 
5286 		override void defaultEventHandler_click(ClickEvent event) {
5287 			if(event.button == MouseButton.left) {
5288 				controller.setSelection(currentIndexLoaded());
5289 				controller.focusOn = currentIndexLoaded();
5290 			}
5291 		}
5292 
5293 	}
5294 
5295 	ListWidget controller;
5296 
5297 	this(ListWidget parent) {
5298 		this.controller = parent;
5299 		this.tabStop = false; // this is only used as a child of the ListWidget
5300 		super(parent);
5301 
5302 		smw.movementPerButtonClick(1, itemSize().height);
5303 	}
5304 
5305 	override Size itemSize() {
5306 		return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */));
5307 	}
5308 
5309 	override GenericListViewItem itemFactory(Widget parent) {
5310 		return new TextListViewItem(controller, parent);
5311 	}
5312 
5313 	static class Style : Widget.Style {
5314 		override FrameStyle borderStyle() {
5315 			return FrameStyle.sunk;
5316 		}
5317 
5318 		override WidgetBackground background() {
5319 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
5320 		}
5321 	}
5322 	mixin OverrideStyle!Style;
5323 }
5324 
5325 /++
5326 	A list widget contains a list of strings that the user can examine and select.
5327 
5328 
5329 	In the future, items in the list may be possible to be more than just strings.
5330 
5331 	See_Also:
5332 		[TableView]
5333 +/
5334 class ListWidget : Widget {
5335 	/// 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.
5336 	mixin Emits!(ChangeEvent!void);
5337 
5338 	version(custom_widgets)
5339 		TextListViewWidget glvw;
5340 
5341 	static struct Option {
5342 		string label;
5343 		bool selected;
5344 		void* tag;
5345 	}
5346 	private Option[] options;
5347 
5348 	/++
5349 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
5350 	+/
5351 	void setSelection(int y) {
5352 		if(!multiSelect)
5353 			foreach(ref opt; options)
5354 				opt.selected = false;
5355 		if(y >= 0 && y < options.length)
5356 			options[y].selected = !options[y].selected;
5357 
5358 		version(custom_widgets)
5359 			focusOn = y;
5360 
5361 		this.emit!(ChangeEvent!void)(delegate {});
5362 
5363 		version(custom_widgets)
5364 			redraw();
5365 	}
5366 
5367 	/++
5368 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
5369 		Returns -1 if nothing is selected.
5370 	+/
5371 	int getSelection()
5372 	{
5373 		foreach(i, opt; options) {
5374 			if (opt.selected)
5375 				return cast(int) i;
5376 		}
5377 		return -1;
5378 	}
5379 
5380 	version(custom_widgets)
5381 	private int focusOn;
5382 
5383 	this(Widget parent) {
5384 		super(parent);
5385 
5386 		version(custom_widgets)
5387 			glvw = new TextListViewWidget(this);
5388 
5389 		version(win32_widgets)
5390 			createWin32Window(this, WC_LISTBOX, "",
5391 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
5392 	}
5393 
5394 	version(win32_widgets)
5395 	override void handleWmCommand(ushort code, ushort id) {
5396 		switch(code) {
5397 			case LBN_SELCHANGE:
5398 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
5399 				setSelection(cast(int) sel);
5400 			break;
5401 			default:
5402 		}
5403 	}
5404 
5405 
5406 	void addOption(string text, void* tag = null) {
5407 		options ~= Option(text, false, tag);
5408 		version(win32_widgets) {
5409 			WCharzBuffer buffer = WCharzBuffer(text);
5410 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
5411 		}
5412 		version(custom_widgets) {
5413 			glvw.setItemCount(cast(int) options.length);
5414 			//setContentSize(width, cast(int) (options.length * defaultLineHeight));
5415 			redraw();
5416 		}
5417 	}
5418 
5419 	void clear() {
5420 		options = null;
5421 		version(win32_widgets) {
5422 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
5423 				{}
5424 
5425 		} else version(custom_widgets) {
5426 			focusOn = -1;
5427 			glvw.setItemCount(0);
5428 			redraw();
5429 		}
5430 	}
5431 
5432 	version(custom_widgets)
5433 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
5434 		void changedFocusOn() {
5435 			scrollFocusIntoView();
5436 			if(multiSelect)
5437 				redraw();
5438 			else
5439 				setSelection(focusOn);
5440 		}
5441 		switch(kde.key) {
5442 			case Key.Up:
5443 				if(focusOn) {
5444 					focusOn--;
5445 					changedFocusOn();
5446 				}
5447 			break;
5448 			case Key.Down:
5449 				if(focusOn + 1 < options.length) {
5450 					focusOn++;
5451 					changedFocusOn();
5452 				}
5453 			break;
5454 			case Key.Home:
5455 				if(focusOn) {
5456 					focusOn = 0;
5457 					changedFocusOn();
5458 				}
5459 			break;
5460 			case Key.End:
5461 				if(options.length && focusOn + 1 != options.length) {
5462 					focusOn = cast(int) options.length - 1;
5463 					changedFocusOn();
5464 				}
5465 			break;
5466 			case Key.PageUp:
5467 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5468 				focusOn -= n;
5469 				if(focusOn < 0)
5470 					focusOn = 0;
5471 				changedFocusOn();
5472 			break;
5473 			case Key.PageDown:
5474 				if(options.length == 0)
5475 					break;
5476 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5477 				focusOn += n;
5478 				if(focusOn >= options.length)
5479 					focusOn = cast(int) options.length - 1;
5480 				changedFocusOn();
5481 			break;
5482 
5483 			default:
5484 		}
5485 	}
5486 
5487 	version(custom_widgets)
5488 	override void defaultEventHandler_char(CharEvent ce) {
5489 		if(ce.character == '\n' || ce.character == ' ') {
5490 			setSelection(focusOn);
5491 		} else {
5492 			// search for the item that best matches and jump to it
5493 			// FIXME this sucks in tons of ways. the normal thing toolkits
5494 			// do here is to search for a substring on a timer, but i'd kinda
5495 			// rather make an actual little dialog with some options. still meh for now.
5496 			dchar search = ce.character;
5497 			if(search >= 'A' && search <= 'Z')
5498 				search += 32;
5499 			foreach(idx, option; options) {
5500 				auto ch = option.label.length ? option.label[0] : 0;
5501 				if(ch >= 'A' && ch <= 'Z')
5502 					ch += 32;
5503 				if(ch == search) {
5504 					setSelection(cast(int) idx);
5505 					scrollSelectionIntoView();
5506 					break;
5507 				}
5508 			}
5509 
5510 		}
5511 	}
5512 
5513 	version(win32_widgets)
5514 		enum multiSelect = false; /// not implemented yet
5515 	else
5516 		bool multiSelect;
5517 
5518 	override int heightStretchiness() { return 6; }
5519 
5520 	version(custom_widgets)
5521 	void scrollFocusIntoView() {
5522 		glvw.ensureItemVisibleInScroll(focusOn);
5523 	}
5524 
5525 	void scrollSelectionIntoView() {
5526 		// FIXME: implement on Windows
5527 
5528 		version(custom_widgets)
5529 			glvw.ensureItemVisibleInScroll(getSelection());
5530 	}
5531 
5532 	/*
5533 	version(custom_widgets)
5534 	override void defaultEventHandler_focusout(Event foe) {
5535 		glvw.redraw();
5536 	}
5537 
5538 	version(custom_widgets)
5539 	override void defaultEventHandler_focusin(Event foe) {
5540 		glvw.redraw();
5541 	}
5542 	*/
5543 
5544 }
5545 
5546 
5547 
5548 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
5549 /// NEVER USED
5550 enum ScrollBarShowPolicy {
5551 	automatic, /// automatically show the scroll bar if it is necessary
5552 	never, /// never show the scroll bar (scrolling must be done programmatically)
5553 	always /// always show the scroll bar, even if it is disabled
5554 }
5555 
5556 /++
5557 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
5558 
5559 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
5560 +/
5561 // FIXME ScrollBarShowPolicy
5562 // FIXME: use the ScrollMessageWidget in here now that it exists
5563 // deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it
5564 class ScrollableWidget : Widget {
5565 	// FIXME: make line size configurable
5566 	// FIXME: add keyboard controls
5567 	version(win32_widgets) {
5568 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
5569 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
5570 				auto pos = HIWORD(wParam);
5571 				auto m = LOWORD(wParam);
5572 
5573 				// FIXME: I can reintroduce the
5574 				// scroll bars now by using this
5575 				// in the top-level window handler
5576 				// to forward comamnds
5577 				auto scrollbarHwnd = lParam;
5578 				switch(m) {
5579 					case SB_BOTTOM:
5580 						if(msg == WM_HSCROLL)
5581 							horizontalScrollTo(contentWidth_);
5582 						else
5583 							verticalScrollTo(contentHeight_);
5584 					break;
5585 					case SB_TOP:
5586 						if(msg == WM_HSCROLL)
5587 							horizontalScrollTo(0);
5588 						else
5589 							verticalScrollTo(0);
5590 					break;
5591 					case SB_ENDSCROLL:
5592 						// idk
5593 					break;
5594 					case SB_LINEDOWN:
5595 						if(msg == WM_HSCROLL)
5596 							horizontalScroll(scaleWithDpi(16));
5597 						else
5598 							verticalScroll(scaleWithDpi(16));
5599 					break;
5600 					case SB_LINEUP:
5601 						if(msg == WM_HSCROLL)
5602 							horizontalScroll(scaleWithDpi(-16));
5603 						else
5604 							verticalScroll(scaleWithDpi(-16));
5605 					break;
5606 					case SB_PAGEDOWN:
5607 						if(msg == WM_HSCROLL)
5608 							horizontalScroll(scaleWithDpi(100));
5609 						else
5610 							verticalScroll(scaleWithDpi(100));
5611 					break;
5612 					case SB_PAGEUP:
5613 						if(msg == WM_HSCROLL)
5614 							horizontalScroll(scaleWithDpi(-100));
5615 						else
5616 							verticalScroll(scaleWithDpi(-100));
5617 					break;
5618 					case SB_THUMBPOSITION:
5619 					case SB_THUMBTRACK:
5620 						if(msg == WM_HSCROLL)
5621 							horizontalScrollTo(pos);
5622 						else
5623 							verticalScrollTo(pos);
5624 
5625 						if(m == SB_THUMBTRACK) {
5626 							// the event loop doesn't seem to carry on with a requested redraw..
5627 							// so we request it to get our dirty bit set...
5628 							redraw();
5629 
5630 							// then we need to immediately actually redraw it too for instant feedback to user
5631 
5632 							SimpleWindow.processAllCustomEvents();
5633 							//if(parentWindow)
5634 								//parentWindow.actualRedraw();
5635 						}
5636 					break;
5637 					default:
5638 				}
5639 			}
5640 			return super.hookedWndProc(msg, wParam, lParam);
5641 		}
5642 	}
5643 	///
5644 	this(Widget parent) {
5645 		this.parentWindow = parent.parentWindow;
5646 
5647 		version(win32_widgets) {
5648 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
5649 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
5650 			super(parent);
5651 		} else version(custom_widgets) {
5652 			outerContainer = new InternalScrollableContainerWidget(this, parent);
5653 			super(outerContainer);
5654 		} else static assert(0);
5655 	}
5656 
5657 	version(custom_widgets)
5658 		InternalScrollableContainerWidget outerContainer;
5659 
5660 	override void defaultEventHandler_click(ClickEvent event) {
5661 		if(event.button == MouseButton.wheelUp)
5662 			verticalScroll(scaleWithDpi(-16));
5663 		if(event.button == MouseButton.wheelDown)
5664 			verticalScroll(scaleWithDpi(16));
5665 		super.defaultEventHandler_click(event);
5666 	}
5667 
5668 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5669 		switch(event.key) {
5670 			case Key.Left:
5671 				horizontalScroll(scaleWithDpi(-16));
5672 			break;
5673 			case Key.Right:
5674 				horizontalScroll(scaleWithDpi(16));
5675 			break;
5676 			case Key.Up:
5677 				verticalScroll(scaleWithDpi(-16));
5678 			break;
5679 			case Key.Down:
5680 				verticalScroll(scaleWithDpi(16));
5681 			break;
5682 			case Key.Home:
5683 				verticalScrollTo(0);
5684 			break;
5685 			case Key.End:
5686 				verticalScrollTo(contentHeight);
5687 			break;
5688 			case Key.PageUp:
5689 				verticalScroll(scaleWithDpi(-160));
5690 			break;
5691 			case Key.PageDown:
5692 				verticalScroll(scaleWithDpi(160));
5693 			break;
5694 			default:
5695 		}
5696 		super.defaultEventHandler_keydown(event);
5697 	}
5698 
5699 
5700 	version(win32_widgets)
5701 	override void recomputeChildLayout() {
5702 		super.recomputeChildLayout();
5703 		SCROLLINFO info;
5704 		info.cbSize = info.sizeof;
5705 		info.nPage = viewportHeight;
5706 		info.fMask = SIF_PAGE | SIF_RANGE;
5707 		info.nMin = 0;
5708 		info.nMax = contentHeight_;
5709 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5710 
5711 		info.cbSize = info.sizeof;
5712 		info.nPage = viewportWidth;
5713 		info.fMask = SIF_PAGE | SIF_RANGE;
5714 		info.nMin = 0;
5715 		info.nMax = contentWidth_;
5716 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5717 	}
5718 
5719 	/*
5720 		Scrolling
5721 		------------
5722 
5723 		You are assigned a width and a height by the layout engine, which
5724 		is your viewport box. However, you may draw more than that by setting
5725 		a contentWidth and contentHeight.
5726 
5727 		If these can be contained by the viewport, no scrollbar is displayed.
5728 		If they cannot fit though, it will automatically show scroll as necessary.
5729 
5730 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5731 		is zero, no vertical scrolling is performed.
5732 
5733 		If scrolling is necessary, the lib will automatically work with the bars.
5734 		When you redraw, the origin and clipping info in the painter is set so if
5735 		you just draw everything, it will work, but you can be more efficient by checking
5736 		the viewportWidth, viewportHeight, and scrollOrigin members.
5737 	*/
5738 
5739 	///
5740 	final @property int viewportWidth() {
5741 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5742 	}
5743 	///
5744 	final @property int viewportHeight() {
5745 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5746 	}
5747 
5748 	// FIXME property
5749 	Point scrollOrigin_;
5750 
5751 	///
5752 	final const(Point) scrollOrigin() {
5753 		return scrollOrigin_;
5754 	}
5755 
5756 	// the user sets these two
5757 	private int contentWidth_ = 0;
5758 	private int contentHeight_ = 0;
5759 
5760 	///
5761 	int contentWidth() { return contentWidth_; }
5762 	///
5763 	int contentHeight() { return contentHeight_; }
5764 
5765 	///
5766 	void setContentSize(int width, int height) {
5767 		contentWidth_ = width;
5768 		contentHeight_ = height;
5769 
5770 		version(custom_widgets) {
5771 			if(showingVerticalScroll || showingHorizontalScroll) {
5772 				outerContainer.queueRecomputeChildLayout();
5773 			}
5774 
5775 			if(showingVerticalScroll())
5776 				outerContainer.verticalScrollBar.redraw();
5777 			if(showingHorizontalScroll())
5778 				outerContainer.horizontalScrollBar.redraw();
5779 		} else version(win32_widgets) {
5780 			queueRecomputeChildLayout();
5781 		} else static assert(0);
5782 	}
5783 
5784 	///
5785 	void verticalScroll(int delta) {
5786 		verticalScrollTo(scrollOrigin.y + delta);
5787 	}
5788 	///
5789 	void verticalScrollTo(int pos) {
5790 		scrollOrigin_.y = pos;
5791 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5792 			scrollOrigin_.y = contentHeight - viewportHeight;
5793 
5794 		if(scrollOrigin_.y < 0)
5795 			scrollOrigin_.y = 0;
5796 
5797 		version(win32_widgets) {
5798 			SCROLLINFO info;
5799 			info.cbSize = info.sizeof;
5800 			info.fMask = SIF_POS;
5801 			info.nPos = scrollOrigin_.y;
5802 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5803 		} else version(custom_widgets) {
5804 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5805 		} else static assert(0);
5806 
5807 		redraw();
5808 	}
5809 
5810 	///
5811 	void horizontalScroll(int delta) {
5812 		horizontalScrollTo(scrollOrigin.x + delta);
5813 	}
5814 	///
5815 	void horizontalScrollTo(int pos) {
5816 		scrollOrigin_.x = pos;
5817 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5818 			scrollOrigin_.x = contentWidth - viewportWidth;
5819 
5820 		if(scrollOrigin_.x < 0)
5821 			scrollOrigin_.x = 0;
5822 
5823 		version(win32_widgets) {
5824 			SCROLLINFO info;
5825 			info.cbSize = info.sizeof;
5826 			info.fMask = SIF_POS;
5827 			info.nPos = scrollOrigin_.x;
5828 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5829 		} else version(custom_widgets) {
5830 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5831 		} else static assert(0);
5832 
5833 		redraw();
5834 	}
5835 	///
5836 	void scrollTo(Point p) {
5837 		verticalScrollTo(p.y);
5838 		horizontalScrollTo(p.x);
5839 	}
5840 
5841 	///
5842 	void ensureVisibleInScroll(Point p) {
5843 		auto rect = viewportRectangle();
5844 		if(rect.contains(p))
5845 			return;
5846 		if(p.x < rect.left)
5847 			horizontalScroll(p.x - rect.left);
5848 		else if(p.x > rect.right)
5849 			horizontalScroll(p.x - rect.right);
5850 
5851 		if(p.y < rect.top)
5852 			verticalScroll(p.y - rect.top);
5853 		else if(p.y > rect.bottom)
5854 			verticalScroll(p.y - rect.bottom);
5855 	}
5856 
5857 	///
5858 	void ensureVisibleInScroll(Rectangle rect) {
5859 		ensureVisibleInScroll(rect.upperLeft);
5860 		ensureVisibleInScroll(rect.lowerRight);
5861 	}
5862 
5863 	///
5864 	Rectangle viewportRectangle() {
5865 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5866 	}
5867 
5868 	///
5869 	bool showingHorizontalScroll() {
5870 		return contentWidth > width;
5871 	}
5872 	///
5873 	bool showingVerticalScroll() {
5874 		return contentHeight > height;
5875 	}
5876 
5877 	/// This is called before the ordinary paint delegate,
5878 	/// giving you a chance to draw the window frame, etc,
5879 	/// before the scroll clip takes effect
5880 	void paintFrameAndBackground(WidgetPainter painter) {
5881 		version(win32_widgets) {
5882 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5883 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5884 			// since the pen is null, to fill the whole space, we need the +1 on both.
5885 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5886 			SelectObject(painter.impl.hdc, p);
5887 			SelectObject(painter.impl.hdc, b);
5888 		}
5889 
5890 	}
5891 
5892 	// make space for the scroll bar, and that's it.
5893 	final override int paddingRight() { return scaleWithDpi(16); }
5894 	final override int paddingBottom() { return scaleWithDpi(16); }
5895 
5896 	/*
5897 		END SCROLLING
5898 	*/
5899 
5900 	override WidgetPainter draw() {
5901 		int x = this.x, y = this.y;
5902 		auto parent = this.parent;
5903 		while(parent) {
5904 			x += parent.x;
5905 			y += parent.y;
5906 			parent = parent.parent;
5907 		}
5908 
5909 		//version(win32_widgets) {
5910 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5911 		//} else {
5912 			auto painter = parentWindow.win.draw(true);
5913 		//}
5914 		painter.originX = x;
5915 		painter.originY = y;
5916 
5917 		painter.originX = painter.originX - scrollOrigin.x;
5918 		painter.originY = painter.originY - scrollOrigin.y;
5919 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
5920 
5921 		return WidgetPainter(painter, this);
5922 	}
5923 
5924 	mixin ScrollableChildren;
5925 }
5926 
5927 // you need to have a Point scrollOrigin in the class somewhere
5928 // and a paintFrameAndBackground
5929 private mixin template ScrollableChildren() {
5930 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
5931 		if(hidden)
5932 			return;
5933 
5934 		//version(win32_widgets)
5935 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
5936 
5937 		painter.originX = lox + x;
5938 		painter.originY = loy + y;
5939 
5940 		bool actuallyPainted = false;
5941 
5942 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
5943 		if(clip == Rectangle.init)
5944 			return;
5945 
5946 		if(force || redrawRequested) {
5947 			//painter.setClipRectangle(scrollOrigin, width, height);
5948 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
5949 			paintFrameAndBackground(painter);
5950 		}
5951 
5952 		/+
5953 		version(win32_widgets) {
5954 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
5955 		}
5956 		+/
5957 
5958 		painter.originX = painter.originX - scrollOrigin.x;
5959 		painter.originY = painter.originY - scrollOrigin.y;
5960 		if(force || redrawRequested) {
5961 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
5962 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
5963 
5964 			//erase(painter); // we paintFrameAndBackground above so no need
5965 			if(painter.visualTheme)
5966 				painter.visualTheme.doPaint(this, painter);
5967 			else
5968 				paint(painter);
5969 
5970 			if(invalidate) {
5971 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
5972 				// children are contained inside this, so no need to do extra work
5973 				invalidate = false;
5974 			}
5975 
5976 
5977 			actuallyPainted = true;
5978 			redrawRequested = false;
5979 		}
5980 
5981 		foreach(child; children) {
5982 			if(cast(FixedPosition) child)
5983 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
5984 			else
5985 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
5986 		}
5987 	}
5988 }
5989 
5990 private class InternalScrollableContainerInsideWidget : ContainerWidget {
5991 	ScrollableContainerWidget scw;
5992 
5993 	this(ScrollableContainerWidget parent) {
5994 		scw = parent;
5995 		super(parent);
5996 	}
5997 
5998 	version(custom_widgets)
5999 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6000 		if(hidden)
6001 			return;
6002 
6003 		bool actuallyPainted = false;
6004 
6005 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
6006 
6007 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
6008 		if(clip == Rectangle.init)
6009 			return;
6010 
6011 		painter.originX = lox + x - scrollOrigin.x;
6012 		painter.originY = loy + y - scrollOrigin.y;
6013 		if(force || redrawRequested) {
6014 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6015 
6016 			erase(painter);
6017 			if(painter.visualTheme)
6018 				painter.visualTheme.doPaint(this, painter);
6019 			else
6020 				paint(painter);
6021 
6022 			if(invalidate) {
6023 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6024 				// children are contained inside this, so no need to do extra work
6025 				invalidate = false;
6026 			}
6027 
6028 			actuallyPainted = true;
6029 			redrawRequested = false;
6030 		}
6031 		foreach(child; children) {
6032 			if(cast(FixedPosition) child)
6033 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6034 			else
6035 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6036 		}
6037 	}
6038 
6039 	version(custom_widgets)
6040 	override protected void addScrollPosition(ref int x, ref int y) {
6041 		x += scw.scrollX_;
6042 		y += scw.scrollY_;
6043 	}
6044 }
6045 
6046 /++
6047 	A widget meant to contain other widgets that may need to scroll.
6048 
6049 	Currently buggy.
6050 
6051 	History:
6052 		Added July 1, 2021 (dub v10.2)
6053 
6054 		On January 3, 2022, I tried to use it in a few other cases
6055 		and found it only worked well in the original test case. Since
6056 		it still sucks, I think I'm going to rewrite it again.
6057 +/
6058 class ScrollableContainerWidget : ContainerWidget {
6059 	///
6060 	this(Widget parent) {
6061 		super(parent);
6062 
6063 		container = new InternalScrollableContainerInsideWidget(this);
6064 		hsb = new HorizontalScrollbar(this);
6065 		vsb = new VerticalScrollbar(this);
6066 
6067 		tabStop = false;
6068 		container.tabStop = false;
6069 		magic = true;
6070 
6071 
6072 		vsb.addEventListener("scrolltonextline", () {
6073 			scrollBy(0, scaleWithDpi(16));
6074 		});
6075 		vsb.addEventListener("scrolltopreviousline", () {
6076 			scrollBy(0,scaleWithDpi( -16));
6077 		});
6078 		vsb.addEventListener("scrolltonextpage", () {
6079 			scrollBy(0, container.height);
6080 		});
6081 		vsb.addEventListener("scrolltopreviouspage", () {
6082 			scrollBy(0, -container.height);
6083 		});
6084 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
6085 			scrollTo(scrollX_, spe.value);
6086 		});
6087 
6088 		this.addEventListener(delegate (scope ClickEvent e) {
6089 			if(e.button == MouseButton.wheelUp) {
6090 				if(!e.defaultPrevented)
6091 					scrollBy(0, scaleWithDpi(-16));
6092 				e.stopPropagation();
6093 			} else if(e.button == MouseButton.wheelDown) {
6094 				if(!e.defaultPrevented)
6095 					scrollBy(0, scaleWithDpi(16));
6096 				e.stopPropagation();
6097 			}
6098 		});
6099 	}
6100 
6101 	/+
6102 	override void defaultEventHandler_click(ClickEvent e) {
6103 	}
6104 	+/
6105 
6106 	override void removeAllChildren() {
6107 		container.removeAllChildren();
6108 	}
6109 
6110 	void scrollTo(int x, int y) {
6111 		scrollBy(x - scrollX_, y - scrollY_);
6112 	}
6113 
6114 	void scrollBy(int x, int y) {
6115 		auto ox = scrollX_;
6116 		auto oy = scrollY_;
6117 
6118 		auto nx = ox + x;
6119 		auto ny = oy + y;
6120 
6121 		if(nx < 0)
6122 			nx = 0;
6123 		if(ny < 0)
6124 			ny = 0;
6125 
6126 		auto maxX = hsb.max - container.width;
6127 		if(maxX < 0) maxX = 0;
6128 		auto maxY = vsb.max - container.height;
6129 		if(maxY < 0) maxY = 0;
6130 
6131 		if(nx > maxX)
6132 			nx = maxX;
6133 		if(ny > maxY)
6134 			ny = maxY;
6135 
6136 		auto dx = nx - ox;
6137 		auto dy = ny - oy;
6138 
6139 		if(dx || dy) {
6140 			version(win32_widgets)
6141 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
6142 			else {
6143 				redraw();
6144 			}
6145 
6146 			hsb.setPosition = nx;
6147 			vsb.setPosition = ny;
6148 
6149 			scrollX_ = nx;
6150 			scrollY_ = ny;
6151 		}
6152 	}
6153 
6154 	private int scrollX_;
6155 	private int scrollY_;
6156 
6157 	void setTotalArea(int width, int height) {
6158 		hsb.setMax(width);
6159 		vsb.setMax(height);
6160 	}
6161 
6162 	///
6163 	void setViewableArea(int width, int height) {
6164 		hsb.setViewableArea(width);
6165 		vsb.setViewableArea(height);
6166 	}
6167 
6168 	private bool magic;
6169 	override void addChild(Widget w, int position = int.max) {
6170 		if(magic)
6171 			container.addChild(w, position);
6172 		else
6173 			super.addChild(w, position);
6174 	}
6175 
6176 	override void recomputeChildLayout() {
6177 		if(hsb is null || vsb is null || container is null) return;
6178 
6179 		/+
6180 		writeln(x, " ", y , " ", width, " ", height);
6181 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
6182 		+/
6183 
6184 		registerMovement();
6185 
6186 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
6187 		hsb.x = 0;
6188 		hsb.y = this.height - hsb.height;
6189 		hsb.width = this.width - scaleWithDpi(16);
6190 		hsb.recomputeChildLayout();
6191 
6192 		vsb.width = scaleWithDpi(16); // FIXME?
6193 		vsb.x = this.width - vsb.width;
6194 		vsb.y = 0;
6195 		vsb.height = this.height - scaleWithDpi(16);
6196 		vsb.recomputeChildLayout();
6197 
6198 		container.x = 0;
6199 		container.y = 0;
6200 		container.width = this.width - vsb.width;
6201 		container.height = this.height - hsb.height;
6202 		container.recomputeChildLayout();
6203 
6204 		scrollX_ = 0;
6205 		scrollY_ = 0;
6206 
6207 		hsb.setPosition(0);
6208 		vsb.setPosition(0);
6209 
6210 		int mw, mh;
6211 		Widget c = container;
6212 		// FIXME: hack here to handle a layout inside...
6213 		if(c.children.length == 1 && cast(Layout) c.children[0])
6214 			c = c.children[0];
6215 		foreach(child; c.children) {
6216 			auto w = child.x + child.width;
6217 			auto h = child.y + child.height;
6218 
6219 			if(w > mw) mw = w;
6220 			if(h > mh) mh = h;
6221 		}
6222 
6223 		setTotalArea(mw, mh);
6224 		setViewableArea(width, height);
6225 	}
6226 
6227 	override int minHeight() { return scaleWithDpi(64); }
6228 
6229 	HorizontalScrollbar hsb;
6230 	VerticalScrollbar vsb;
6231 	ContainerWidget container;
6232 }
6233 
6234 
6235 version(custom_widgets)
6236 // deprecated // i can't deprecate it w/o stupid messages ugh
6237 private class InternalScrollableContainerWidget : Widget {
6238 
6239 	ScrollableWidget sw;
6240 
6241 	VerticalScrollbar verticalScrollBar;
6242 	HorizontalScrollbar horizontalScrollBar;
6243 
6244 	this(ScrollableWidget sw, Widget parent) {
6245 		this.sw = sw;
6246 
6247 		this.tabStop = false;
6248 
6249 		super(parent);
6250 
6251 		horizontalScrollBar = new HorizontalScrollbar(this);
6252 		verticalScrollBar = new VerticalScrollbar(this);
6253 
6254 		horizontalScrollBar.showing_ = false;
6255 		verticalScrollBar.showing_ = false;
6256 
6257 		horizontalScrollBar.addEventListener("scrolltonextline", {
6258 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
6259 			sw.horizontalScrollTo(horizontalScrollBar.position);
6260 		});
6261 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
6262 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
6263 			sw.horizontalScrollTo(horizontalScrollBar.position);
6264 		});
6265 		verticalScrollBar.addEventListener("scrolltonextline", {
6266 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
6267 			sw.verticalScrollTo(verticalScrollBar.position);
6268 		});
6269 		verticalScrollBar.addEventListener("scrolltopreviousline", {
6270 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
6271 			sw.verticalScrollTo(verticalScrollBar.position);
6272 		});
6273 		horizontalScrollBar.addEventListener("scrolltonextpage", {
6274 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
6275 			sw.horizontalScrollTo(horizontalScrollBar.position);
6276 		});
6277 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
6278 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
6279 			sw.horizontalScrollTo(horizontalScrollBar.position);
6280 		});
6281 		verticalScrollBar.addEventListener("scrolltonextpage", {
6282 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
6283 			sw.verticalScrollTo(verticalScrollBar.position);
6284 		});
6285 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
6286 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
6287 			sw.verticalScrollTo(verticalScrollBar.position);
6288 		});
6289 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
6290 			horizontalScrollBar.setPosition(event.intValue);
6291 			sw.horizontalScrollTo(horizontalScrollBar.position);
6292 		});
6293 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
6294 			verticalScrollBar.setPosition(event.intValue);
6295 			sw.verticalScrollTo(verticalScrollBar.position);
6296 		});
6297 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
6298 			horizontalScrollBar.setPosition(event.intValue);
6299 			sw.horizontalScrollTo(horizontalScrollBar.position);
6300 		});
6301 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
6302 			verticalScrollBar.setPosition(event.intValue);
6303 		});
6304 	}
6305 
6306 	// this is supposed to be basically invisible...
6307 	override int minWidth() { return sw.minWidth; }
6308 	override int minHeight() { return sw.minHeight; }
6309 	override int maxWidth() { return sw.maxWidth; }
6310 	override int maxHeight() { return sw.maxHeight; }
6311 	override int widthStretchiness() { return sw.widthStretchiness; }
6312 	override int heightStretchiness() { return sw.heightStretchiness; }
6313 	override int marginLeft() { return sw.marginLeft; }
6314 	override int marginRight() { return sw.marginRight; }
6315 	override int marginTop() { return sw.marginTop; }
6316 	override int marginBottom() { return sw.marginBottom; }
6317 	override int paddingLeft() { return sw.paddingLeft; }
6318 	override int paddingRight() { return sw.paddingRight; }
6319 	override int paddingTop() { return sw.paddingTop; }
6320 	override int paddingBottom() { return sw.paddingBottom; }
6321 	override void focus() { sw.focus(); }
6322 
6323 
6324 	override void recomputeChildLayout() {
6325 		// The stupid thing needs to calculate if a scroll bar is needed...
6326 		recomputeChildLayoutHelper();
6327 		// then running it again will position things correctly if the bar is NOT needed
6328 		recomputeChildLayoutHelper();
6329 
6330 		// this sucks but meh it barely works
6331 	}
6332 
6333 	private void recomputeChildLayoutHelper() {
6334 		if(sw is null) return;
6335 
6336 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
6337 		if(horizontalScrollBar && verticalScrollBar) {
6338 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
6339 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
6340 			horizontalScrollBar.x = 0;
6341 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
6342 
6343 			verticalScrollBar.width = verticalScrollBar.minWidth();
6344 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
6345 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
6346 			verticalScrollBar.y = 0 + 2;
6347 
6348 			sw.x = 0;
6349 			sw.y = 0;
6350 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
6351 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
6352 
6353 			if(sw.contentWidth_ <= this.width)
6354 				sw.scrollOrigin_.x = 0;
6355 			if(sw.contentHeight_ <= this.height)
6356 				sw.scrollOrigin_.y = 0;
6357 
6358 			horizontalScrollBar.recomputeChildLayout();
6359 			verticalScrollBar.recomputeChildLayout();
6360 			sw.recomputeChildLayout();
6361 		}
6362 
6363 		if(sw.contentWidth_ <= this.width)
6364 			sw.scrollOrigin_.x = 0;
6365 		if(sw.contentHeight_ <= this.height)
6366 			sw.scrollOrigin_.y = 0;
6367 
6368 		if(sw.showingHorizontalScroll())
6369 			horizontalScrollBar.showing(true, false);
6370 		else
6371 			horizontalScrollBar.showing(false, false);
6372 		if(sw.showingVerticalScroll())
6373 			verticalScrollBar.showing(true, false);
6374 		else
6375 			verticalScrollBar.showing(false, false);
6376 
6377 		verticalScrollBar.setViewableArea(sw.viewportHeight());
6378 		verticalScrollBar.setMax(sw.contentHeight);
6379 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
6380 
6381 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
6382 		horizontalScrollBar.setMax(sw.contentWidth);
6383 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
6384 	}
6385 }
6386 
6387 /*
6388 class ScrollableClientWidget : Widget {
6389 	this(Widget parent) {
6390 		super(parent);
6391 	}
6392 	override void paint(WidgetPainter p) {
6393 		parent.paint(p);
6394 	}
6395 }
6396 */
6397 
6398 /++
6399 	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.
6400 +/
6401 abstract class Slider : Widget {
6402 	this(int min, int max, int step, Widget parent) {
6403 		min_ = min;
6404 		max_ = max;
6405 		step_ = step;
6406 		page_ = step;
6407 		super(parent);
6408 	}
6409 
6410 	private int min_;
6411 	private int max_;
6412 	private int step_;
6413 	private int position_;
6414 	private int page_;
6415 
6416 	// selection start and selection end
6417 	// tics
6418 	// tooltip?
6419 	// some way to see and just type the value
6420 	// win32 buddy controls are labels
6421 
6422 	///
6423 	void setMin(int a) {
6424 		min_ = a;
6425 		version(custom_widgets)
6426 			redraw();
6427 		version(win32_widgets)
6428 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
6429 	}
6430 	///
6431 	int min() {
6432 		return min_;
6433 	}
6434 	///
6435 	void setMax(int a) {
6436 		max_ = a;
6437 		version(custom_widgets)
6438 			redraw();
6439 		version(win32_widgets)
6440 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
6441 	}
6442 	///
6443 	int max() {
6444 		return max_;
6445 	}
6446 	///
6447 	void setPosition(int a) {
6448 		if(a > max)
6449 			a = max;
6450 		if(a < min)
6451 			a = min;
6452 		position_ = a;
6453 		version(custom_widgets)
6454 			setPositionCustom(a);
6455 
6456 		version(win32_widgets)
6457 			setPositionWindows(a);
6458 	}
6459 	version(win32_widgets) {
6460 		protected abstract void setPositionWindows(int a);
6461 	}
6462 
6463 	protected abstract int win32direction();
6464 
6465 	/++
6466 		Alias for [position] for better compatibility with generic code.
6467 
6468 		History:
6469 			Added October 5, 2021
6470 	+/
6471 	@property int value() {
6472 		return position;
6473 	}
6474 
6475 	///
6476 	int position() {
6477 		return position_;
6478 	}
6479 	///
6480 	void setStep(int a) {
6481 		step_ = a;
6482 		version(win32_widgets)
6483 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
6484 	}
6485 	///
6486 	int step() {
6487 		return step_;
6488 	}
6489 	///
6490 	void setPageSize(int a) {
6491 		page_ = a;
6492 		version(win32_widgets)
6493 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
6494 	}
6495 	///
6496 	int pageSize() {
6497 		return page_;
6498 	}
6499 
6500 	private void notify() {
6501 		auto event = new ChangeEvent!int(this, &this.position);
6502 		event.dispatch();
6503 	}
6504 
6505 	version(win32_widgets)
6506 	void win32Setup(int style) {
6507 		createWin32Window(this, TRACKBAR_CLASS, "",
6508 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
6509 
6510 		// the trackbar sends the same messages as scroll, which
6511 		// our other layer sends as these... just gonna translate
6512 		// here
6513 		this.addDirectEventListener("scrolltoposition", (Event event) {
6514 			event.stopPropagation();
6515 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
6516 			notify();
6517 		});
6518 		this.addDirectEventListener("scrolltonextline", (Event event) {
6519 			event.stopPropagation();
6520 			this.setPosition(this.position + this.step_ * this.win32direction);
6521 			notify();
6522 		});
6523 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
6524 			event.stopPropagation();
6525 			this.setPosition(this.position - this.step_ * this.win32direction);
6526 			notify();
6527 		});
6528 		this.addDirectEventListener("scrolltonextpage", (Event event) {
6529 			event.stopPropagation();
6530 			this.setPosition(this.position + this.page_ * this.win32direction);
6531 			notify();
6532 		});
6533 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
6534 			event.stopPropagation();
6535 			this.setPosition(this.position - this.page_ * this.win32direction);
6536 			notify();
6537 		});
6538 
6539 		setMin(min_);
6540 		setMax(max_);
6541 		setStep(step_);
6542 		setPageSize(page_);
6543 	}
6544 
6545 	version(custom_widgets) {
6546 		protected MouseTrackingWidget thumb;
6547 
6548 		protected abstract void setPositionCustom(int a);
6549 
6550 		override void defaultEventHandler_keydown(KeyDownEvent event) {
6551 			switch(event.key) {
6552 				case Key.Up:
6553 				case Key.Right:
6554 					setPosition(position() - step() * win32direction);
6555 					changed();
6556 				break;
6557 				case Key.Down:
6558 				case Key.Left:
6559 					setPosition(position() + step() * win32direction);
6560 					changed();
6561 				break;
6562 				case Key.Home:
6563 					setPosition(win32direction > 0 ? min() : max());
6564 					changed();
6565 				break;
6566 				case Key.End:
6567 					setPosition(win32direction > 0 ? max() : min());
6568 					changed();
6569 				break;
6570 				case Key.PageUp:
6571 					setPosition(position() - pageSize() * win32direction);
6572 					changed();
6573 				break;
6574 				case Key.PageDown:
6575 					setPosition(position() + pageSize() * win32direction);
6576 					changed();
6577 				break;
6578 				default:
6579 			}
6580 			super.defaultEventHandler_keydown(event);
6581 		}
6582 
6583 		protected void changed() {
6584 			auto ev = new ChangeEvent!int(this, &position);
6585 			ev.dispatch();
6586 		}
6587 	}
6588 }
6589 
6590 /++
6591 
6592 +/
6593 class VerticalSlider : Slider {
6594 	this(int min, int max, int step, Widget parent) {
6595 		version(custom_widgets)
6596 			initialize();
6597 
6598 		super(min, max, step, parent);
6599 
6600 		version(win32_widgets)
6601 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
6602 	}
6603 
6604 	protected override int win32direction() {
6605 		return -1;
6606 	}
6607 
6608 	version(win32_widgets)
6609 	protected override void setPositionWindows(int a) {
6610 		// the windows thing makes the top 0 and i don't like that.
6611 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
6612 	}
6613 
6614 	version(custom_widgets)
6615 	private void initialize() {
6616 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
6617 
6618 		thumb.tabStop = false;
6619 
6620 		thumb.thumbWidth = width;
6621 		thumb.thumbHeight = scaleWithDpi(16);
6622 
6623 		thumb.addEventListener(EventType.change, () {
6624 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
6625 			sx = max - sx;
6626 			//informProgramThatUserChangedPosition(sx);
6627 
6628 			position_ = sx;
6629 
6630 			changed();
6631 		});
6632 	}
6633 
6634 	version(custom_widgets)
6635 	override void recomputeChildLayout() {
6636 		thumb.thumbWidth = this.width;
6637 		super.recomputeChildLayout();
6638 		setPositionCustom(position_);
6639 	}
6640 
6641 	version(custom_widgets)
6642 	protected override void setPositionCustom(int a) {
6643 		if(max())
6644 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
6645 		redraw();
6646 	}
6647 }
6648 
6649 /++
6650 
6651 +/
6652 class HorizontalSlider : Slider {
6653 	this(int min, int max, int step, Widget parent) {
6654 		version(custom_widgets)
6655 			initialize();
6656 
6657 		super(min, max, step, parent);
6658 
6659 		version(win32_widgets)
6660 			win32Setup(TBS_HORZ);
6661 	}
6662 
6663 	version(win32_widgets)
6664 	protected override void setPositionWindows(int a) {
6665 		SendMessage(hwnd, TBM_SETPOS, true, a);
6666 	}
6667 
6668 	protected override int win32direction() {
6669 		return 1;
6670 	}
6671 
6672 	version(custom_widgets)
6673 	private void initialize() {
6674 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6675 
6676 		thumb.tabStop = false;
6677 
6678 		thumb.thumbWidth = scaleWithDpi(16);
6679 		thumb.thumbHeight = height;
6680 
6681 		thumb.addEventListener(EventType.change, () {
6682 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6683 			//informProgramThatUserChangedPosition(sx);
6684 
6685 			position_ = sx;
6686 
6687 			changed();
6688 		});
6689 	}
6690 
6691 	version(custom_widgets)
6692 	override void recomputeChildLayout() {
6693 		thumb.thumbHeight = this.height;
6694 		super.recomputeChildLayout();
6695 		setPositionCustom(position_);
6696 	}
6697 
6698 	version(custom_widgets)
6699 	protected override void setPositionCustom(int a) {
6700 		if(max())
6701 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6702 		redraw();
6703 	}
6704 }
6705 
6706 
6707 ///
6708 abstract class ScrollbarBase : Widget {
6709 	///
6710 	this(Widget parent) {
6711 		super(parent);
6712 		tabStop = false;
6713 		step_ = scaleWithDpi(16);
6714 	}
6715 
6716 	private int viewableArea_;
6717 	private int max_;
6718 	private int step_;// = 16;
6719 	private int position_;
6720 
6721 	///
6722 	bool atEnd() {
6723 		return position_ + viewableArea_ >= max_;
6724 	}
6725 
6726 	///
6727 	bool atStart() {
6728 		return position_ == 0;
6729 	}
6730 
6731 	///
6732 	void setViewableArea(int a) {
6733 		viewableArea_ = a;
6734 		version(custom_widgets)
6735 			redraw();
6736 	}
6737 	///
6738 	void setMax(int a) {
6739 		max_ = a;
6740 		version(custom_widgets)
6741 			redraw();
6742 	}
6743 	///
6744 	int max() {
6745 		return max_;
6746 	}
6747 	///
6748 	void setPosition(int a) {
6749 		auto logicalMax = max_ - viewableArea_;
6750 		if(a == int.max)
6751 			a = logicalMax;
6752 
6753 		if(a > logicalMax)
6754 			a = logicalMax;
6755 		if(a < 0)
6756 			a = 0;
6757 
6758 		position_ = a;
6759 
6760 		version(custom_widgets)
6761 			redraw();
6762 	}
6763 	///
6764 	int position() {
6765 		return position_;
6766 	}
6767 	///
6768 	void setStep(int a) {
6769 		step_ = a;
6770 	}
6771 	///
6772 	int step() {
6773 		return step_;
6774 	}
6775 
6776 	// FIXME: remove this.... maybe
6777 	/+
6778 	protected void informProgramThatUserChangedPosition(int n) {
6779 		position_ = n;
6780 		auto evt = new Event(EventType.change, this);
6781 		evt.intValue = n;
6782 		evt.dispatch();
6783 	}
6784 	+/
6785 
6786 	version(custom_widgets) {
6787 		enum MIN_THUMB_SIZE = 8;
6788 
6789 		abstract protected int getBarDim();
6790 		int thumbSize() {
6791 			if(viewableArea_ >= max_ || max_ == 0)
6792 				return getBarDim();
6793 
6794 			int res = viewableArea_ * getBarDim() / max_;
6795 
6796 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6797 				res = scaleWithDpi(MIN_THUMB_SIZE);
6798 
6799 			return res;
6800 		}
6801 
6802 		int thumbPosition() {
6803 			/*
6804 				viewableArea_ is the viewport height/width
6805 				position_ is where we are
6806 			*/
6807 			//if(position_ + viewableArea_ >= max_)
6808 				//return getBarDim - thumbSize;
6809 
6810 			auto maximumPossibleValue = getBarDim() - thumbSize;
6811 			auto maximiumLogicalValue = max_ - viewableArea_;
6812 
6813 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6814 
6815 			return p;
6816 		}
6817 	}
6818 }
6819 
6820 //public import mgt;
6821 
6822 /++
6823 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6824 
6825 	Concrete subclasses may include a scrollbar thumb and a volume control.
6826 +/
6827 //version(custom_widgets)
6828 class MouseTrackingWidget : Widget {
6829 
6830 	///
6831 	int positionX() { return positionX_; }
6832 	///
6833 	int positionY() { return positionY_; }
6834 
6835 	///
6836 	void positionX(int p) { positionX_ = p; }
6837 	///
6838 	void positionY(int p) { positionY_ = p; }
6839 
6840 	private int positionX_;
6841 	private int positionY_;
6842 
6843 	///
6844 	enum Orientation {
6845 		horizontal, ///
6846 		vertical, ///
6847 		twoDimensional, ///
6848 	}
6849 
6850 	private int thumbWidth_;
6851 	private int thumbHeight_;
6852 
6853 	///
6854 	int thumbWidth() { return thumbWidth_; }
6855 	///
6856 	int thumbHeight() { return thumbHeight_; }
6857 	///
6858 	int thumbWidth(int a) { return thumbWidth_ = a; }
6859 	///
6860 	int thumbHeight(int a) { return thumbHeight_ = a; }
6861 
6862 	private bool dragging;
6863 	private bool hovering;
6864 	private int startMouseX, startMouseY;
6865 
6866 	///
6867 	this(Orientation orientation, Widget parent) {
6868 		super(parent);
6869 
6870 		//assert(parentWindow !is null);
6871 
6872 		addEventListener((MouseDownEvent event) {
6873 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6874 				dragging = true;
6875 				startMouseX = event.clientX - positionX;
6876 				startMouseY = event.clientY - positionY;
6877 				parentWindow.captureMouse(this);
6878 			} else {
6879 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6880 					positionX = event.clientX - thumbWidth / 2;
6881 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6882 					positionY = event.clientY - thumbHeight / 2;
6883 
6884 				if(positionX + thumbWidth > this.width)
6885 					positionX = this.width - thumbWidth;
6886 				if(positionY + thumbHeight > this.height)
6887 					positionY = this.height - thumbHeight;
6888 
6889 				if(positionX < 0)
6890 					positionX = 0;
6891 				if(positionY < 0)
6892 					positionY = 0;
6893 
6894 
6895 				// this.emit!(ChangeEvent!void)();
6896 				auto evt = new Event(EventType.change, this);
6897 				evt.sendDirectly();
6898 
6899 				redraw();
6900 
6901 			}
6902 		});
6903 
6904 		addEventListener(EventType.mouseup, (Event event) {
6905 			dragging = false;
6906 			parentWindow.releaseMouseCapture();
6907 		});
6908 
6909 		addEventListener(EventType.mouseout, (Event event) {
6910 			if(!hovering)
6911 				return;
6912 			hovering = false;
6913 			redraw();
6914 		});
6915 
6916 		int lpx, lpy;
6917 
6918 		addEventListener((MouseMoveEvent event) {
6919 			auto oh = hovering;
6920 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6921 				hovering = true;
6922 			} else {
6923 				hovering = false;
6924 			}
6925 			if(!dragging) {
6926 				if(hovering != oh)
6927 					redraw();
6928 				return;
6929 			}
6930 
6931 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6932 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
6933 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6934 				positionY = event.clientY - startMouseY;
6935 
6936 			if(positionX + thumbWidth > this.width)
6937 				positionX = this.width - thumbWidth;
6938 			if(positionY + thumbHeight > this.height)
6939 				positionY = this.height - thumbHeight;
6940 
6941 			if(positionX < 0)
6942 				positionX = 0;
6943 			if(positionY < 0)
6944 				positionY = 0;
6945 
6946 			if(positionX != lpx || positionY != lpy) {
6947 				lpx = positionX;
6948 				lpy = positionY;
6949 
6950 				auto evt = new Event(EventType.change, this);
6951 				evt.sendDirectly();
6952 			}
6953 
6954 			redraw();
6955 		});
6956 	}
6957 
6958 	version(custom_widgets)
6959 	override void paint(WidgetPainter painter) {
6960 		auto cs = getComputedStyle();
6961 		auto c = darken(cs.windowBackgroundColor, 0.2);
6962 		painter.outlineColor = c;
6963 		painter.fillColor = c;
6964 		painter.drawRectangle(Point(0, 0), this.width, this.height);
6965 
6966 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
6967 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
6968 	}
6969 }
6970 
6971 //version(custom_widgets)
6972 //private
6973 class HorizontalScrollbar : ScrollbarBase {
6974 
6975 	version(custom_widgets) {
6976 		private MouseTrackingWidget thumb;
6977 
6978 		override int getBarDim() {
6979 			return thumb.width;
6980 		}
6981 	}
6982 
6983 	override void setViewableArea(int a) {
6984 		super.setViewableArea(a);
6985 
6986 		version(win32_widgets) {
6987 			SCROLLINFO info;
6988 			info.cbSize = info.sizeof;
6989 			info.nPage = a + 1;
6990 			info.fMask = SIF_PAGE;
6991 			SetScrollInfo(hwnd, SB_CTL, &info, true);
6992 		} else version(custom_widgets) {
6993 			thumb.positionX = thumbPosition;
6994 			thumb.thumbWidth = thumbSize;
6995 			thumb.redraw();
6996 		} else static assert(0);
6997 
6998 	}
6999 
7000 	override void setMax(int a) {
7001 		super.setMax(a);
7002 		version(win32_widgets) {
7003 			SCROLLINFO info;
7004 			info.cbSize = info.sizeof;
7005 			info.nMin = 0;
7006 			info.nMax = max;
7007 			info.fMask = SIF_RANGE;
7008 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7009 		} else version(custom_widgets) {
7010 			thumb.positionX = thumbPosition;
7011 			thumb.thumbWidth = thumbSize;
7012 			thumb.redraw();
7013 		}
7014 	}
7015 
7016 	override void setPosition(int a) {
7017 		super.setPosition(a);
7018 		version(win32_widgets) {
7019 			SCROLLINFO info;
7020 			info.cbSize = info.sizeof;
7021 			info.fMask = SIF_POS;
7022 			info.nPos = position;
7023 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7024 		} else version(custom_widgets) {
7025 			thumb.positionX = thumbPosition();
7026 			thumb.thumbWidth = thumbSize;
7027 			thumb.redraw();
7028 		} else static assert(0);
7029 	}
7030 
7031 	this(Widget parent) {
7032 		super(parent);
7033 
7034 		version(win32_widgets) {
7035 			createWin32Window(this, "Scrollbar"w, "",
7036 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
7037 		} else version(custom_widgets) {
7038 			auto vl = new HorizontalLayout(this);
7039 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
7040 			leftButton.setClickRepeat(scrollClickRepeatInterval);
7041 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
7042 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
7043 			rightButton.setClickRepeat(scrollClickRepeatInterval);
7044 
7045 			leftButton.tabStop = false;
7046 			rightButton.tabStop = false;
7047 			thumb.tabStop = false;
7048 
7049 			leftButton.addEventListener(EventType.triggered, () {
7050 				this.emitCommand!"scrolltopreviousline"();
7051 				//informProgramThatUserChangedPosition(position - step());
7052 			});
7053 			rightButton.addEventListener(EventType.triggered, () {
7054 				this.emitCommand!"scrolltonextline"();
7055 				//informProgramThatUserChangedPosition(position + step());
7056 			});
7057 
7058 			thumb.thumbWidth = this.minWidth;
7059 			thumb.thumbHeight = scaleWithDpi(16);
7060 
7061 			thumb.addEventListener(EventType.change, () {
7062 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
7063 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
7064 
7065 				//informProgramThatUserChangedPosition(sx);
7066 
7067 				auto ev = new ScrollToPositionEvent(this, sx);
7068 				ev.dispatch();
7069 			});
7070 		}
7071 	}
7072 
7073 	override int minHeight() { return scaleWithDpi(16); }
7074 	override int maxHeight() { return scaleWithDpi(16); }
7075 	override int minWidth() { return scaleWithDpi(48); }
7076 }
7077 
7078 class ScrollToPositionEvent : Event {
7079 	enum EventString = "scrolltoposition";
7080 
7081 	this(Widget target, int value) {
7082 		this.value = value;
7083 		super(EventString, target);
7084 	}
7085 
7086 	immutable int value;
7087 
7088 	override @property int intValue() {
7089 		return value;
7090 	}
7091 }
7092 
7093 //version(custom_widgets)
7094 //private
7095 class VerticalScrollbar : ScrollbarBase {
7096 
7097 	version(custom_widgets) {
7098 		override int getBarDim() {
7099 			return thumb.height;
7100 		}
7101 
7102 		private MouseTrackingWidget thumb;
7103 	}
7104 
7105 	override void setViewableArea(int a) {
7106 		super.setViewableArea(a);
7107 
7108 		version(win32_widgets) {
7109 			SCROLLINFO info;
7110 			info.cbSize = info.sizeof;
7111 			info.nPage = a + 1;
7112 			info.fMask = SIF_PAGE;
7113 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7114 		} else version(custom_widgets) {
7115 			thumb.positionY = thumbPosition;
7116 			thumb.thumbHeight = thumbSize;
7117 			thumb.redraw();
7118 		} else static assert(0);
7119 
7120 	}
7121 
7122 	override void setMax(int a) {
7123 		super.setMax(a);
7124 		version(win32_widgets) {
7125 			SCROLLINFO info;
7126 			info.cbSize = info.sizeof;
7127 			info.nMin = 0;
7128 			info.nMax = max;
7129 			info.fMask = SIF_RANGE;
7130 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7131 		} else version(custom_widgets) {
7132 			thumb.positionY = thumbPosition;
7133 			thumb.thumbHeight = thumbSize;
7134 			thumb.redraw();
7135 		}
7136 	}
7137 
7138 	override void setPosition(int a) {
7139 		super.setPosition(a);
7140 		version(win32_widgets) {
7141 			SCROLLINFO info;
7142 			info.cbSize = info.sizeof;
7143 			info.fMask = SIF_POS;
7144 			info.nPos = position;
7145 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7146 		} else version(custom_widgets) {
7147 			thumb.positionY = thumbPosition;
7148 			thumb.thumbHeight = thumbSize;
7149 			thumb.redraw();
7150 		} else static assert(0);
7151 	}
7152 
7153 	this(Widget parent) {
7154 		super(parent);
7155 
7156 		version(win32_widgets) {
7157 			createWin32Window(this, "Scrollbar"w, "",
7158 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
7159 		} else version(custom_widgets) {
7160 			auto vl = new VerticalLayout(this);
7161 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
7162 			upButton.setClickRepeat(scrollClickRepeatInterval);
7163 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
7164 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
7165 			downButton.setClickRepeat(scrollClickRepeatInterval);
7166 
7167 			upButton.addEventListener(EventType.triggered, () {
7168 				this.emitCommand!"scrolltopreviousline"();
7169 				//informProgramThatUserChangedPosition(position - step());
7170 			});
7171 			downButton.addEventListener(EventType.triggered, () {
7172 				this.emitCommand!"scrolltonextline"();
7173 				//informProgramThatUserChangedPosition(position + step());
7174 			});
7175 
7176 			thumb.thumbWidth = this.minWidth;
7177 			thumb.thumbHeight = scaleWithDpi(16);
7178 
7179 			thumb.addEventListener(EventType.change, () {
7180 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
7181 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
7182 
7183 				auto ev = new ScrollToPositionEvent(this, sy);
7184 				ev.dispatch();
7185 
7186 				//informProgramThatUserChangedPosition(sy);
7187 			});
7188 
7189 			upButton.tabStop = false;
7190 			downButton.tabStop = false;
7191 			thumb.tabStop = false;
7192 		}
7193 	}
7194 
7195 	override int minWidth() { return scaleWithDpi(16); }
7196 	override int maxWidth() { return scaleWithDpi(16); }
7197 	override int minHeight() { return scaleWithDpi(48); }
7198 }
7199 
7200 
7201 /++
7202 	EXPERIMENTAL
7203 
7204 	A widget specialized for being a container for other widgets.
7205 
7206 	History:
7207 		Added May 29, 2021. Not stabilized at this time.
7208 +/
7209 class WidgetContainer : Widget {
7210 	this(Widget parent) {
7211 		tabStop = false;
7212 		super(parent);
7213 	}
7214 
7215 	override int maxHeight() {
7216 		if(this.children.length == 1) {
7217 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
7218 		} else {
7219 			return int.max;
7220 		}
7221 	}
7222 
7223 	override int maxWidth() {
7224 		if(this.children.length == 1) {
7225 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
7226 		} else {
7227 			return int.max;
7228 		}
7229 	}
7230 
7231 	/+
7232 
7233 	override int minHeight() {
7234 		int largest = 0;
7235 		int margins = 0;
7236 		int lastMargin = 0;
7237 		foreach(child; children) {
7238 			auto mh = child.minHeight();
7239 			if(mh > largest)
7240 				largest = mh;
7241 			margins += mymax(lastMargin, child.marginTop());
7242 			lastMargin = child.marginBottom();
7243 		}
7244 		return largest + margins;
7245 	}
7246 
7247 	override int maxHeight() {
7248 		int largest = 0;
7249 		int margins = 0;
7250 		int lastMargin = 0;
7251 		foreach(child; children) {
7252 			auto mh = child.maxHeight();
7253 			if(mh == int.max)
7254 				return int.max;
7255 			if(mh > largest)
7256 				largest = mh;
7257 			margins += mymax(lastMargin, child.marginTop());
7258 			lastMargin = child.marginBottom();
7259 		}
7260 		return largest + margins;
7261 	}
7262 
7263 	override int minWidth() {
7264 		int min;
7265 		foreach(child; children) {
7266 			auto cm = child.minWidth;
7267 			if(cm > min)
7268 				min = cm;
7269 		}
7270 		return min + paddingLeft + paddingRight;
7271 	}
7272 
7273 	override int minHeight() {
7274 		int min;
7275 		foreach(child; children) {
7276 			auto cm = child.minHeight;
7277 			if(cm > min)
7278 				min = cm;
7279 		}
7280 		return min + paddingTop + paddingBottom;
7281 	}
7282 
7283 	override int maxHeight() {
7284 		int largest = 0;
7285 		int margins = 0;
7286 		int lastMargin = 0;
7287 		foreach(child; children) {
7288 			auto mh = child.maxHeight();
7289 			if(mh == int.max)
7290 				return int.max;
7291 			if(mh > largest)
7292 				largest = mh;
7293 			margins += mymax(lastMargin, child.marginTop());
7294 			lastMargin = child.marginBottom();
7295 		}
7296 		return largest + margins;
7297 	}
7298 
7299 	override int heightStretchiness() {
7300 		int max;
7301 		foreach(child; children) {
7302 			auto c = child.heightStretchiness;
7303 			if(c > max)
7304 				max = c;
7305 		}
7306 		return max;
7307 	}
7308 
7309 	override int marginTop() {
7310 		if(this.children.length)
7311 			return this.children[0].marginTop;
7312 		return 0;
7313 	}
7314 	+/
7315 }
7316 
7317 ///
7318 abstract class Layout : Widget {
7319 	this(Widget parent) {
7320 		tabStop = false;
7321 		super(parent);
7322 	}
7323 }
7324 
7325 /++
7326 	Makes all children minimum width and height, placing them down
7327 	left to right, top to bottom.
7328 
7329 	Useful if you want to make a list of buttons that automatically
7330 	wrap to a new line when necessary.
7331 +/
7332 class InlineBlockLayout : Layout {
7333 	///
7334 	this(Widget parent) { super(parent); }
7335 
7336 	override void recomputeChildLayout() {
7337 		registerMovement();
7338 
7339 		int x = this.paddingLeft, y = this.paddingTop;
7340 
7341 		int lineHeight;
7342 		int previousMargin = 0;
7343 		int previousMarginBottom = 0;
7344 
7345 		foreach(child; children) {
7346 			if(child.hidden)
7347 				continue;
7348 			if(cast(FixedPosition) child) {
7349 				child.recomputeChildLayout();
7350 				continue;
7351 			}
7352 			child.width = child.flexBasisWidth();
7353 			if(child.width == 0)
7354 				child.width = child.minWidth();
7355 			if(child.width == 0)
7356 				child.width = 32;
7357 
7358 			child.height = child.flexBasisHeight();
7359 			if(child.height == 0)
7360 				child.height = child.minHeight();
7361 			if(child.height == 0)
7362 				child.height = 32;
7363 
7364 			if(x + child.width + paddingRight > this.width) {
7365 				x = this.paddingLeft;
7366 				y += lineHeight;
7367 				lineHeight = 0;
7368 				previousMargin = 0;
7369 				previousMarginBottom = 0;
7370 			}
7371 
7372 			auto margin = child.marginLeft;
7373 			if(previousMargin > margin)
7374 				margin = previousMargin;
7375 
7376 			x += margin;
7377 
7378 			child.x = x;
7379 			child.y = y;
7380 
7381 			int marginTopApplied;
7382 			if(child.marginTop > previousMarginBottom) {
7383 				child.y += child.marginTop;
7384 				marginTopApplied = child.marginTop;
7385 			}
7386 
7387 			x += child.width;
7388 			previousMargin = child.marginRight;
7389 
7390 			if(child.marginBottom > previousMarginBottom)
7391 				previousMarginBottom = child.marginBottom;
7392 
7393 			auto h = child.height + previousMarginBottom + marginTopApplied;
7394 			if(h > lineHeight)
7395 				lineHeight = h;
7396 
7397 			child.recomputeChildLayout();
7398 		}
7399 
7400 	}
7401 
7402 	override int minWidth() {
7403 		int min;
7404 		foreach(child; children) {
7405 			auto cm = child.minWidth;
7406 			if(cm > min)
7407 				min = cm;
7408 		}
7409 		return min + paddingLeft + paddingRight;
7410 	}
7411 
7412 	override int minHeight() {
7413 		int min;
7414 		foreach(child; children) {
7415 			auto cm = child.minHeight;
7416 			if(cm > min)
7417 				min = cm;
7418 		}
7419 		return min + paddingTop + paddingBottom;
7420 	}
7421 }
7422 
7423 /++
7424 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
7425 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
7426 	the [TabWidget] will automatically change pages of child widgets.
7427 
7428 	This allows you to react to it however you see fit rather than having to
7429 	be tied to just the new sets of child widgets.
7430 
7431 	It sends the message in the form of `this.emitCommand!"changetab"();`.
7432 
7433 	History:
7434 		Added December 24, 2021 (dub v10.5)
7435 +/
7436 class TabMessageWidget : Widget {
7437 
7438 	protected void tabIndexClicked(int item) {
7439 		this.emitCommand!"changetab"();
7440 	}
7441 
7442 	/++
7443 		Adds the a new tab to the control with the given title.
7444 
7445 		Returns:
7446 			The index of the newly added tab. You will need to know
7447 			this index to refer to it later and to know which tab to
7448 			change to when you get a changetab message.
7449 	+/
7450 	int addTab(string title, int pos = int.max) {
7451 		version(win32_widgets) {
7452 			TCITEM item;
7453 			item.mask = TCIF_TEXT;
7454 			WCharzBuffer buf = WCharzBuffer(title);
7455 			item.pszText = buf.ptr;
7456 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
7457 		} else version(custom_widgets) {
7458 			if(pos >= tabs.length) {
7459 				tabs ~= title;
7460 				redraw();
7461 				return cast(int) tabs.length - 1;
7462 			} else if(pos <= 0) {
7463 				tabs = title ~ tabs;
7464 				redraw();
7465 				return 0;
7466 			} else {
7467 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
7468 				redraw();
7469 				return pos;
7470 			}
7471 		}
7472 	}
7473 
7474 	override void addChild(Widget child, int pos = int.max) {
7475 		if(container)
7476 			container.addChild(child, pos);
7477 		else
7478 			super.addChild(child, pos);
7479 	}
7480 
7481 	protected Widget makeContainer() {
7482 		return new Widget(this);
7483 	}
7484 
7485 	private Widget container;
7486 
7487 	override void recomputeChildLayout() {
7488 		version(win32_widgets) {
7489 			this.registerMovement();
7490 
7491 			RECT rect;
7492 			GetWindowRect(hwnd, &rect);
7493 
7494 			auto left = rect.left;
7495 			auto top = rect.top;
7496 
7497 			TabCtrl_AdjustRect(hwnd, false, &rect);
7498 			foreach(child; children) {
7499 				if(!child.showing) continue;
7500 				child.x = rect.left - left;
7501 				child.y = rect.top - top;
7502 				child.width = rect.right - rect.left;
7503 				child.height = rect.bottom - rect.top;
7504 				child.recomputeChildLayout();
7505 			}
7506 		} else version(custom_widgets) {
7507 			this.registerMovement();
7508 			foreach(child; children) {
7509 				if(!child.showing) continue;
7510 				child.x = 2;
7511 				child.y = tabBarHeight + 2; // for the border
7512 				child.width = width - 4; // for the border
7513 				child.height = height - tabBarHeight - 2 - 2; // for the border
7514 				child.recomputeChildLayout();
7515 			}
7516 		} else static assert(0);
7517 	}
7518 
7519 	version(custom_widgets)
7520 		string[] tabs;
7521 
7522 	this(Widget parent) {
7523 		super(parent);
7524 
7525 		tabStop = false;
7526 
7527 		version(win32_widgets) {
7528 			createWin32Window(this, WC_TABCONTROL, "", 0);
7529 		} else version(custom_widgets) {
7530 			addEventListener((ClickEvent event) {
7531 				if(event.target !is this)
7532 					return;
7533 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
7534 					auto t = (event.clientX / tabWidth);
7535 					if(t >= 0 && t < tabs.length) {
7536 						currentTab_ = t;
7537 						tabIndexClicked(t);
7538 						redraw();
7539 					}
7540 				}
7541 			});
7542 		} else static assert(0);
7543 
7544 		this.container = makeContainer();
7545 	}
7546 
7547 	override int marginTop() { return 4; }
7548 	override int paddingBottom() { return 4; }
7549 
7550 	override int minHeight() {
7551 		int max = 0;
7552 		foreach(child; children)
7553 			max = mymax(child.minHeight, max);
7554 
7555 
7556 		version(win32_widgets) {
7557 			RECT rect;
7558 			rect.right = this.width;
7559 			rect.bottom = max;
7560 			TabCtrl_AdjustRect(hwnd, true, &rect);
7561 
7562 			max = rect.bottom;
7563 		} else {
7564 			max += defaultLineHeight + 4;
7565 		}
7566 
7567 
7568 		return max;
7569 	}
7570 
7571 	version(win32_widgets)
7572 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
7573 		switch(code) {
7574 			case TCN_SELCHANGE:
7575 				auto sel = TabCtrl_GetCurSel(hwnd);
7576 				tabIndexClicked(sel);
7577 			break;
7578 			default:
7579 		}
7580 		return 0;
7581 	}
7582 
7583 	version(custom_widgets) {
7584 		private int currentTab_;
7585 		private int tabBarHeight() { return defaultLineHeight; }
7586 		int tabWidth() { return scaleWithDpi(80); }
7587 	}
7588 
7589 	version(win32_widgets)
7590 	override void paint(WidgetPainter painter) {}
7591 
7592 	version(custom_widgets)
7593 	override void paint(WidgetPainter painter) {
7594 		auto cs = getComputedStyle();
7595 
7596 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
7597 
7598 		int posX = 0;
7599 		foreach(idx, title; tabs) {
7600 			auto isCurrent = idx == getCurrentTab();
7601 
7602 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
7603 
7604 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
7605 			painter.outlineColor = cs.foregroundColor;
7606 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
7607 
7608 			if(isCurrent) {
7609 				painter.outlineColor = cs.windowBackgroundColor;
7610 				painter.fillColor = Color.transparent;
7611 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
7612 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
7613 
7614 				painter.outlineColor = Color.white;
7615 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
7616 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
7617 				painter.outlineColor = cs.activeTabColor;
7618 				painter.drawPixel(Point(posX, tabBarHeight - 1));
7619 			}
7620 
7621 			posX += tabWidth - 2;
7622 		}
7623 	}
7624 
7625 	///
7626 	@scriptable
7627 	void setCurrentTab(int item) {
7628 		version(win32_widgets)
7629 			TabCtrl_SetCurSel(hwnd, item);
7630 		else version(custom_widgets)
7631 			currentTab_ = item;
7632 		else static assert(0);
7633 
7634 		tabIndexClicked(item);
7635 	}
7636 
7637 	///
7638 	@scriptable
7639 	int getCurrentTab() {
7640 		version(win32_widgets)
7641 			return TabCtrl_GetCurSel(hwnd);
7642 		else version(custom_widgets)
7643 			return currentTab_; // FIXME
7644 		else static assert(0);
7645 	}
7646 
7647 	///
7648 	@scriptable
7649 	void removeTab(int item) {
7650 		if(item && item == getCurrentTab())
7651 			setCurrentTab(item - 1);
7652 
7653 		version(win32_widgets) {
7654 			TabCtrl_DeleteItem(hwnd, item);
7655 		}
7656 
7657 		for(int a = item; a < children.length - 1; a++)
7658 			this._children[a] = this._children[a + 1];
7659 		this._children = this._children[0 .. $-1];
7660 	}
7661 
7662 }
7663 
7664 
7665 /++
7666 	A tab widget is a set of clickable tab buttons followed by a content area.
7667 
7668 
7669 	Tabs can change existing content or can be new pages.
7670 
7671 	When the user picks a different tab, a `change` message is generated.
7672 +/
7673 class TabWidget : TabMessageWidget {
7674 	this(Widget parent) {
7675 		super(parent);
7676 	}
7677 
7678 	override protected Widget makeContainer() {
7679 		return null;
7680 	}
7681 
7682 	override void addChild(Widget child, int pos = int.max) {
7683 		if(auto twp = cast(TabWidgetPage) child) {
7684 			Widget.addChild(child, pos);
7685 			if(pos == int.max)
7686 				pos = cast(int) this.children.length - 1;
7687 
7688 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7689 
7690 			if(pos != getCurrentTab) {
7691 				child.showing = false;
7692 			}
7693 		} else {
7694 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7695 		}
7696 	}
7697 
7698 	// FIXME: add tab icons at some point, Windows supports them
7699 	/++
7700 		Adds a page and its associated tab with the given label to the widget.
7701 
7702 		Returns:
7703 			The added page object, to which you can add other widgets.
7704 	+/
7705 	@scriptable
7706 	TabWidgetPage addPage(string title) {
7707 		return new TabWidgetPage(title, this);
7708 	}
7709 
7710 	/++
7711 		Gets the page at the given tab index, or `null` if the index is bad.
7712 
7713 		History:
7714 			Added December 24, 2021.
7715 	+/
7716 	TabWidgetPage getPage(int index) {
7717 		if(index < this.children.length)
7718 			return null;
7719 		return cast(TabWidgetPage) this.children[index];
7720 	}
7721 
7722 	/++
7723 		While you can still use the addTab from the parent class,
7724 		*strongly* recommend you use [addPage] insteaad.
7725 
7726 		History:
7727 			Added December 24, 2021 to fulful the interface
7728 			requirement that came from adding [TabMessageWidget].
7729 
7730 			You should not use it though since the [addPage] function
7731 			is much easier to use here.
7732 	+/
7733 	override int addTab(string title, int pos = int.max) {
7734 		auto p = addPage(title);
7735 		foreach(idx, child; this.children)
7736 			if(child is p)
7737 				return cast(int) idx;
7738 		return -1;
7739 	}
7740 
7741 	protected override void tabIndexClicked(int item) {
7742 		foreach(idx, child; children) {
7743 			child.showing(false, false); // batch the recalculates for the end
7744 		}
7745 
7746 		foreach(idx, child; children) {
7747 			if(idx == item) {
7748 				child.showing(true, false);
7749 				if(parentWindow) {
7750 					auto f = parentWindow.getFirstFocusable(child);
7751 					if(f)
7752 						f.focus();
7753 				}
7754 				recomputeChildLayout();
7755 			}
7756 		}
7757 
7758 		version(win32_widgets) {
7759 			InvalidateRect(hwnd, null, true);
7760 		} else version(custom_widgets) {
7761 			this.redraw();
7762 		}
7763 	}
7764 
7765 }
7766 
7767 /++
7768 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7769 
7770 	You add [TabWidgetPage]s to it.
7771 +/
7772 class PageWidget : Widget {
7773 	this(Widget parent) {
7774 		super(parent);
7775 	}
7776 
7777 	override int minHeight() {
7778 		int max = 0;
7779 		foreach(child; children)
7780 			max = mymax(child.minHeight, max);
7781 
7782 		return max;
7783 	}
7784 
7785 
7786 	override void addChild(Widget child, int pos = int.max) {
7787 		if(auto twp = cast(TabWidgetPage) child) {
7788 			super.addChild(child, pos);
7789 			if(pos == int.max)
7790 				pos = cast(int) this.children.length - 1;
7791 
7792 			if(pos != getCurrentTab) {
7793 				child.showing = false;
7794 			}
7795 		} else {
7796 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7797 		}
7798 	}
7799 
7800 	override void recomputeChildLayout() {
7801 		this.registerMovement();
7802 		foreach(child; children) {
7803 			child.x = 0;
7804 			child.y = 0;
7805 			child.width = width;
7806 			child.height = height;
7807 			child.recomputeChildLayout();
7808 		}
7809 	}
7810 
7811 	private int currentTab_;
7812 
7813 	///
7814 	@scriptable
7815 	void setCurrentTab(int item) {
7816 		currentTab_ = item;
7817 
7818 		showOnly(item);
7819 	}
7820 
7821 	///
7822 	@scriptable
7823 	int getCurrentTab() {
7824 		return currentTab_;
7825 	}
7826 
7827 	///
7828 	@scriptable
7829 	void removeTab(int item) {
7830 		if(item && item == getCurrentTab())
7831 			setCurrentTab(item - 1);
7832 
7833 		for(int a = item; a < children.length - 1; a++)
7834 			this._children[a] = this._children[a + 1];
7835 		this._children = this._children[0 .. $-1];
7836 	}
7837 
7838 	///
7839 	@scriptable
7840 	TabWidgetPage addPage(string title) {
7841 		return new TabWidgetPage(title, this);
7842 	}
7843 
7844 	private void showOnly(int item) {
7845 		foreach(idx, child; children)
7846 			if(idx == item) {
7847 				child.show();
7848 				child.queueRecomputeChildLayout();
7849 			} else {
7850 				child.hide();
7851 			}
7852 	}
7853 }
7854 
7855 /++
7856 
7857 +/
7858 class TabWidgetPage : Widget {
7859 	string title;
7860 	this(string title, Widget parent) {
7861 		this.title = title;
7862 		this.tabStop = false;
7863 		super(parent);
7864 
7865 		///*
7866 		version(win32_widgets) {
7867 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7868 		}
7869 		//*/
7870 	}
7871 
7872 	override int minHeight() {
7873 		int sum = 0;
7874 		foreach(child; children)
7875 			sum += child.minHeight();
7876 		return sum;
7877 	}
7878 }
7879 
7880 version(none)
7881 /++
7882 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7883 
7884 	I think I need to modify the layout algorithms to support this.
7885 +/
7886 class CollapsableSidebar : Widget {
7887 
7888 }
7889 
7890 /// Stacks the widgets vertically, taking all the available width for each child.
7891 class VerticalLayout : Layout {
7892 	// most of this is intentionally blank - widget's default is vertical layout right now
7893 	///
7894 	this(Widget parent) { super(parent); }
7895 
7896 	/++
7897 		Sets a max width for the layout so you don't have to subclass. The max width
7898 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7899 
7900 		History:
7901 			Added November 29, 2021 (dub v10.5)
7902 	+/
7903 	this(int maxWidth, Widget parent) {
7904 		this.mw = maxWidth;
7905 		super(parent);
7906 	}
7907 
7908 	private int mw = int.max;
7909 
7910 	override int maxWidth() { return scaleWithDpi(mw); }
7911 }
7912 
7913 /// Stacks the widgets horizontally, taking all the available height for each child.
7914 class HorizontalLayout : Layout {
7915 	///
7916 	this(Widget parent) { super(parent); }
7917 
7918 	/++
7919 		Sets a max height for the layout so you don't have to subclass. The max height
7920 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7921 
7922 		History:
7923 			Added November 29, 2021 (dub v10.5)
7924 	+/
7925 	this(int maxHeight, Widget parent) {
7926 		this.mh = maxHeight;
7927 		super(parent);
7928 	}
7929 
7930 	private int mh = 0;
7931 
7932 
7933 
7934 	override void recomputeChildLayout() {
7935 		.recomputeChildLayout!"width"(this);
7936 	}
7937 
7938 	override int minHeight() {
7939 		int largest = 0;
7940 		int margins = 0;
7941 		int lastMargin = 0;
7942 		foreach(child; children) {
7943 			auto mh = child.minHeight();
7944 			if(mh > largest)
7945 				largest = mh;
7946 			margins += mymax(lastMargin, child.marginTop());
7947 			lastMargin = child.marginBottom();
7948 		}
7949 		return largest + margins;
7950 	}
7951 
7952 	override int maxHeight() {
7953 		if(mh != 0)
7954 			return mymax(minHeight, scaleWithDpi(mh));
7955 
7956 		int largest = 0;
7957 		int margins = 0;
7958 		int lastMargin = 0;
7959 		foreach(child; children) {
7960 			auto mh = child.maxHeight();
7961 			if(mh == int.max)
7962 				return int.max;
7963 			if(mh > largest)
7964 				largest = mh;
7965 			margins += mymax(lastMargin, child.marginTop());
7966 			lastMargin = child.marginBottom();
7967 		}
7968 		return largest + margins;
7969 	}
7970 
7971 	override int heightStretchiness() {
7972 		int max;
7973 		foreach(child; children) {
7974 			auto c = child.heightStretchiness;
7975 			if(c > max)
7976 				max = c;
7977 		}
7978 		return max;
7979 	}
7980 }
7981 
7982 version(win32_widgets)
7983 private
7984 extern(Windows)
7985 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
7986 	Widget* pwin = hwnd in Widget.nativeMapping;
7987 	if(pwin is null)
7988 		return DefWindowProc(hwnd, message, wparam, lparam);
7989 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
7990 	if(win is null)
7991 		return DefWindowProc(hwnd, message, wparam, lparam);
7992 
7993 	switch(message) {
7994 		case WM_SIZE:
7995 			auto width = LOWORD(lparam);
7996 			auto height = HIWORD(lparam);
7997 
7998 			auto hdc = GetDC(hwnd);
7999 			auto hdcBmp = CreateCompatibleDC(hdc);
8000 
8001 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
8002 			if(width > win.bmpWidth || height > win.bmpHeight) {
8003 				auto oldBuffer = win.buffer;
8004 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
8005 
8006 				if(oldBuffer)
8007 					DeleteObject(oldBuffer);
8008 
8009 				win.bmpWidth = width;
8010 				win.bmpHeight = height;
8011 			}
8012 
8013 			// just always erase it upon resizing so minigui can draw over with a clean slate
8014 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
8015 
8016 			auto brush = GetSysColorBrush(COLOR_3DFACE);
8017 			RECT r;
8018 			r.left = 0;
8019 			r.top = 0;
8020 			r.right = width;
8021 			r.bottom = height;
8022 			FillRect(hdcBmp, &r, brush);
8023 
8024 			SelectObject(hdcBmp, oldBmp);
8025 			DeleteDC(hdcBmp);
8026 			ReleaseDC(hwnd, hdc);
8027 		break;
8028 		case WM_PAINT:
8029 			if(win.buffer is null)
8030 				goto default;
8031 
8032 			BITMAP bm;
8033 			PAINTSTRUCT ps;
8034 
8035 			HDC hdc = BeginPaint(hwnd, &ps);
8036 
8037 			HDC hdcMem = CreateCompatibleDC(hdc);
8038 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
8039 
8040 			GetObject(win.buffer, bm.sizeof, &bm);
8041 
8042 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
8043 
8044 			SelectObject(hdcMem, hbmOld);
8045 			DeleteDC(hdcMem);
8046 			EndPaint(hwnd, &ps);
8047 		break;
8048 		default:
8049 			return DefWindowProc(hwnd, message, wparam, lparam);
8050 	}
8051 
8052 	return 0;
8053 }
8054 
8055 private wstring Win32Class(wstring name)() {
8056 	static bool classRegistered;
8057 	if(!classRegistered) {
8058 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
8059 		WNDCLASSEX wc;
8060 		wc.cbSize = wc.sizeof;
8061 		wc.hInstance = hInstance;
8062 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
8063 		wc.lpfnWndProc = &DoubleBufferWndProc;
8064 		wc.lpszClassName = name.ptr;
8065 		if(!RegisterClassExW(&wc))
8066 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
8067 		classRegistered = true;
8068 	}
8069 
8070 		return name;
8071 }
8072 
8073 /+
8074 version(win32_widgets)
8075 extern(Windows)
8076 private
8077 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
8078 	switch(iMessage) {
8079 		case WM_PAINT:
8080 			if(auto te = hWnd in Widget.nativeMapping) {
8081 				try {
8082 					//te.redraw();
8083 					writeln(te, " drawing");
8084 				} catch(Exception) {}
8085 			}
8086 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8087 		default:
8088 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8089 	}
8090 }
8091 +/
8092 
8093 
8094 /++
8095 	A widget specifically designed to hold other widgets.
8096 
8097 	History:
8098 		Added July 1, 2021
8099 +/
8100 class ContainerWidget : Widget {
8101 	this(Widget parent) {
8102 		super(parent);
8103 		this.tabStop = false;
8104 
8105 		version(win32_widgets) {
8106 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
8107 		}
8108 	}
8109 }
8110 
8111 /++
8112 	A widget that takes your widget, puts scroll bars around it, and sends
8113 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
8114 	no effort to automatically scroll or clip its child widgets - it just sends
8115 	the messages.
8116 
8117 
8118 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
8119 	The scroll coordinates are all given in a unit you interpret as you wish. One
8120 	of these units is moved on each press of the arrow buttons and represents the
8121 	smallest amount the user can scroll. The intention is for this to be one line,
8122 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
8123 	in each direction that the user might be interested in.
8124 
8125 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
8126 	This is the amount it jumps when the user pressed page up and page down, or clicks
8127 	in the exposed part of the scroll bar.
8128 
8129 	You should add child content to the ScrollMessageWidget. However, it is important to
8130 	note that the coordinates are always independent of the scroll position! It is YOUR
8131 	responsibility to do any necessary transforms, clipping, etc., while drawing the
8132 	content and interpreting mouse events if they are supposed to change with the scroll.
8133 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
8134 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
8135 	you more control (which can be considerably more efficient and adapted to your actual data)
8136 	at the expense of you also needing to be aware of its reality.
8137 
8138 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
8139 	version 10.3. Maybe this will change in the future.... but for now you must call
8140 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
8141 +/
8142 class ScrollMessageWidget : Widget {
8143 	this(Widget parent) {
8144 		super(parent);
8145 
8146 		container = new Widget(this);
8147 		hsb = new HorizontalScrollbar(this);
8148 		vsb = new VerticalScrollbar(this);
8149 
8150 		hsb.addEventListener("scrolltonextline", {
8151 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
8152 			notify();
8153 		});
8154 		hsb.addEventListener("scrolltopreviousline", {
8155 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
8156 			notify();
8157 		});
8158 		vsb.addEventListener("scrolltonextline", {
8159 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
8160 			notify();
8161 		});
8162 		vsb.addEventListener("scrolltopreviousline", {
8163 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
8164 			notify();
8165 		});
8166 		hsb.addEventListener("scrolltonextpage", {
8167 			hsb.setPosition(hsb.position + hsb.step_);
8168 			notify();
8169 		});
8170 		hsb.addEventListener("scrolltopreviouspage", {
8171 			hsb.setPosition(hsb.position - hsb.step_);
8172 			notify();
8173 		});
8174 		vsb.addEventListener("scrolltonextpage", {
8175 			vsb.setPosition(vsb.position + vsb.step_);
8176 			notify();
8177 		});
8178 		vsb.addEventListener("scrolltopreviouspage", {
8179 			vsb.setPosition(vsb.position - vsb.step_);
8180 			notify();
8181 		});
8182 		hsb.addEventListener("scrolltoposition", (Event event) {
8183 			hsb.setPosition(event.intValue);
8184 			notify();
8185 		});
8186 		vsb.addEventListener("scrolltoposition", (Event event) {
8187 			vsb.setPosition(event.intValue);
8188 			notify();
8189 		});
8190 
8191 
8192 		tabStop = false;
8193 		container.tabStop = false;
8194 		magic = true;
8195 	}
8196 
8197 	private int movementPerButtonClickH_ = 1;
8198 	private int movementPerButtonClickV_ = 1;
8199 	public void movementPerButtonClick(int h, int v) {
8200 		movementPerButtonClickH_ = h;
8201 		movementPerButtonClickV_ = v;
8202 	}
8203 
8204 	/++
8205 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
8206 
8207 
8208 		The defaults for [addDefaultWheelListeners] are:
8209 
8210 			$(LIST
8211 				* Mouse wheel scrolls vertically
8212 				* Alt key + mouse wheel scrolls horiontally
8213 				* Shift + mouse wheel scrolls faster.
8214 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
8215 			)
8216 
8217 		The defaults for [addDefaultKeyboardListeners] are:
8218 
8219 			$(LIST
8220 				* Arrow keys scroll by the given amounts
8221 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
8222 				* Page up and down scroll by the vertical viewable area
8223 				* Home and end scroll to the start and end of the verticle viewable area.
8224 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
8225 			)
8226 
8227 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
8228 
8229 		Params:
8230 			horizontalArrowScrollAmount =
8231 			verticalArrowScrollAmount =
8232 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
8233 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
8234 			shiftMultiplier = multiplies the scroll amount by this when shift is held
8235 	+/
8236 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
8237 		defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount;
8238 		defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount;
8239 		defaultKeyboardListener_shiftMultiplier = shiftMultiplier;
8240 
8241 		container.addEventListener(&defaultKeyboardListener);
8242 	}
8243 
8244 	/// ditto
8245 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
8246 		auto _this = this;
8247 		container.addEventListener((scope ClickEvent ce) {
8248 
8249 			//if(ce.target && ce.target.tabStop)
8250 				//ce.target.focus();
8251 
8252 			// ctrl is reserved for the application
8253 			if(ce.ctrlKey)
8254 				return;
8255 
8256 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
8257 				return;
8258 
8259 			if(shiftMultiplier == 0 && ce.shiftKey)
8260 				return;
8261 
8262 			if(ce.button == MouseButton.wheelDown) {
8263 				if(ce.altKey)
8264 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8265 				else
8266 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8267 			} else if(ce.button == MouseButton.wheelUp) {
8268 				if(ce.altKey)
8269 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8270 				else
8271 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8272 			}
8273 		});
8274 	}
8275 
8276 	int defaultKeyboardListener_verticalArrowScrollAmount = 1;
8277 	int defaultKeyboardListener_horizontalArrowScrollAmount = 1;
8278 	int defaultKeyboardListener_shiftMultiplier = 3;
8279 
8280 	void defaultKeyboardListener(scope KeyDownEvent ke) {
8281 		switch(ke.key) {
8282 			case Key.Left:
8283 				this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8284 			break;
8285 			case Key.Right:
8286 				this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8287 			break;
8288 			case Key.Up:
8289 				this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8290 			break;
8291 			case Key.Down:
8292 				this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8293 			break;
8294 			case Key.PageUp:
8295 				if(ke.altKey)
8296 					this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8297 				else
8298 					this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8299 			break;
8300 			case Key.PageDown:
8301 				if(ke.altKey)
8302 					this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8303 				else
8304 					this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8305 			break;
8306 			case Key.Home:
8307 				if(ke.altKey)
8308 					this.scrollLeft(short.max * 16);
8309 				else
8310 					this.scrollUp(short.max * 16);
8311 			break;
8312 			case Key.End:
8313 				if(ke.altKey)
8314 					this.scrollRight(short.max * 16);
8315 				else
8316 					this.scrollDown(short.max * 16);
8317 			break;
8318 
8319 			default:
8320 				// ignore, not for us.
8321 		}
8322 	}
8323 
8324 	/++
8325 		Scrolls the given amount.
8326 
8327 		History:
8328 			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.
8329 	+/
8330 	void scrollUp(int amount = 1) {
8331 		vsb.setPosition(vsb.position.NonOverflowingInt - amount);
8332 		notify();
8333 	}
8334 	/// ditto
8335 	void scrollDown(int amount = 1) {
8336 		vsb.setPosition(vsb.position.NonOverflowingInt + amount);
8337 		notify();
8338 	}
8339 	/// ditto
8340 	void scrollLeft(int amount = 1) {
8341 		hsb.setPosition(hsb.position.NonOverflowingInt - amount);
8342 		notify();
8343 	}
8344 	/// ditto
8345 	void scrollRight(int amount = 1) {
8346 		hsb.setPosition(hsb.position.NonOverflowingInt + amount);
8347 		notify();
8348 	}
8349 
8350 	///
8351 	VerticalScrollbar verticalScrollBar() { return vsb; }
8352 	///
8353 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
8354 
8355 	void notify() {
8356 		static bool insideNotify;
8357 
8358 		if(insideNotify)
8359 			return; // avoid the recursive call, even if it isn't strictly correct
8360 
8361 		insideNotify = true;
8362 		scope(exit) insideNotify = false;
8363 
8364 		this.emit!ScrollEvent();
8365 	}
8366 
8367 	mixin Emits!ScrollEvent;
8368 
8369 	///
8370 	Point position() {
8371 		return Point(hsb.position, vsb.position);
8372 	}
8373 
8374 	///
8375 	void setPosition(int x, int y) {
8376 		hsb.setPosition(x);
8377 		vsb.setPosition(y);
8378 	}
8379 
8380 	///
8381 	void setPageSize(int unitsX, int unitsY) {
8382 		hsb.setStep(unitsX);
8383 		vsb.setStep(unitsY);
8384 	}
8385 
8386 	/// Always call this BEFORE setViewableArea
8387 	void setTotalArea(int width, int height) {
8388 		hsb.setMax(width);
8389 		vsb.setMax(height);
8390 	}
8391 
8392 	/++
8393 		Always set the viewable area AFTER setitng the total area if you are going to change both.
8394 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
8395 		If you need to do that, use [queueRecomputeChildLayout].
8396 	+/
8397 	void setViewableArea(int width, int height) {
8398 
8399 		// actually there IS A need to dothis cuz the max might have changed since then
8400 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
8401 			//return; // no need to do what is already done
8402 		hsb.setViewableArea(width);
8403 		vsb.setViewableArea(height);
8404 
8405 		bool needsNotify = false;
8406 
8407 		// FIXME: if at any point the rhs is outside the scrollbar, we need
8408 		// to reset to 0. but it should remember the old position in case the
8409 		// window resizes again, so it can kinda return ot where it was.
8410 		//
8411 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
8412 		if(width >= hsb.max) {
8413 			// there's plenty of room to display it all so we need to reset to zero
8414 			// FIXME: adjust so it matches the note above
8415 			hsb.setPosition(0);
8416 			needsNotify = true;
8417 		}
8418 		if(height >= vsb.max) {
8419 			// there's plenty of room to display it all so we need to reset to zero
8420 			// FIXME: adjust so it matches the note above
8421 			vsb.setPosition(0);
8422 			needsNotify = true;
8423 		}
8424 		if(needsNotify)
8425 			notify();
8426 	}
8427 
8428 	private bool magic;
8429 	override void addChild(Widget w, int position = int.max) {
8430 		if(magic)
8431 			container.addChild(w, position);
8432 		else
8433 			super.addChild(w, position);
8434 	}
8435 
8436 	override void recomputeChildLayout() {
8437 		if(hsb is null || vsb is null || container is null) return;
8438 
8439 		registerMovement();
8440 
8441 		enum BUTTON_SIZE = 16;
8442 
8443 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
8444 		hsb.x = 0;
8445 		hsb.y = this.height - hsb.height;
8446 
8447 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
8448 		vsb.x = this.width - vsb.width;
8449 		vsb.y = 0;
8450 
8451 		auto vsb_width = vsb.showing ? vsb.width : 0;
8452 		auto hsb_height = hsb.showing ? hsb.height : 0;
8453 
8454 		hsb.width = this.width - vsb_width;
8455 		vsb.height = this.height - hsb_height;
8456 
8457 		hsb.recomputeChildLayout();
8458 		vsb.recomputeChildLayout();
8459 
8460 		if(this.header is null) {
8461 			container.x = 0;
8462 			container.y = 0;
8463 			container.width = this.width - vsb_width;
8464 			container.height = this.height - hsb_height;
8465 			container.recomputeChildLayout();
8466 		} else {
8467 			header.x = 0;
8468 			header.y = 0;
8469 			header.width = this.width - vsb_width;
8470 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
8471 			header.recomputeChildLayout();
8472 
8473 			container.x = 0;
8474 			container.y = scaleWithDpi(BUTTON_SIZE);
8475 			container.width = this.width - vsb_width;
8476 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
8477 			container.recomputeChildLayout();
8478 		}
8479 	}
8480 
8481 	private HorizontalScrollbar hsb;
8482 	private VerticalScrollbar vsb;
8483 	Widget container;
8484 	private Widget header;
8485 
8486 	/++
8487 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
8488 
8489 		History:
8490 			Added September 27, 2021 (dub v10.3)
8491 	+/
8492 	Widget getHeader() {
8493 		if(this.header is null) {
8494 			magic = false;
8495 			scope(exit) magic = true;
8496 			this.header = new Widget(this);
8497 			queueRecomputeChildLayout();
8498 		}
8499 		return this.header;
8500 	}
8501 
8502 	/++
8503 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
8504 
8505 		History:
8506 			Added January 3, 2023 (dub v11.0)
8507 	+/
8508 	void scrollIntoView(Rectangle rect) {
8509 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
8510 
8511 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
8512 
8513 		// the lower right is exclusive normally
8514 		auto test = rect.lowerRight;
8515 		if(test.x > 0) test.x--;
8516 		if(test.y > 0) test.y--;
8517 
8518 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
8519 			// try to scroll only one dimension at a time if we can
8520 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
8521 				setPosition(rect.upperLeft.x, position.y);
8522 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
8523 				setPosition(position.x, rect.upperLeft.y);
8524 		}
8525 
8526 	}
8527 
8528 	override int minHeight() {
8529 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
8530 		if(header !is null)
8531 			min += header.minHeight;
8532 		if(horizontalScrollBar.showing)
8533 			min += horizontalScrollBar.minHeight;
8534 		return min;
8535 	}
8536 
8537 	override int maxHeight() {
8538 		int max = container ? container.maxHeight : int.max;
8539 		if(max == int.max)
8540 			return max;
8541 		if(horizontalScrollBar.showing)
8542 			max += horizontalScrollBar.minHeight;
8543 		return max;
8544 	}
8545 
8546 	static class Style : Widget.Style {
8547 		override WidgetBackground background() {
8548 			return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8549 		}
8550 	}
8551 	mixin OverrideStyle!Style;
8552 }
8553 
8554 /++
8555 	$(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")
8556 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
8557 +/
8558 version(minigui_screenshots)
8559 @Screenshot("ScrollMessageWidget")
8560 unittest {
8561 	auto window = new Window("ScrollMessageWidget");
8562 
8563 	auto smw = new ScrollMessageWidget(window);
8564 	smw.addDefaultKeyboardListeners();
8565 	smw.addDefaultWheelListeners();
8566 
8567 	window.loop();
8568 }
8569 
8570 /++
8571 	Bypasses automatic layout for its children, using manual positioning and sizing only.
8572 	While you need to manually position them, you must ensure they are inside the StaticLayout's
8573 	bounding box to avoid undefined behavior.
8574 
8575 	You should almost never use this.
8576 +/
8577 class StaticLayout : Layout {
8578 	///
8579 	this(Widget parent) { super(parent); }
8580 	override void recomputeChildLayout() {
8581 		registerMovement();
8582 		foreach(child; children)
8583 			child.recomputeChildLayout();
8584 	}
8585 }
8586 
8587 /++
8588 	Bypasses automatic positioning when being laid out. It is your responsibility to make
8589 	room for this widget in the parent layout.
8590 
8591 	Its children are laid out normally, unless there is exactly one, in which case it takes
8592 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
8593 	can do that with `padding`).
8594 +/
8595 class StaticPosition : Layout {
8596 	///
8597 	this(Widget parent) { super(parent); }
8598 
8599 	override void recomputeChildLayout() {
8600 		registerMovement();
8601 		if(this.children.length == 1) {
8602 			auto child = children[0];
8603 			child.x = 0;
8604 			child.y = 0;
8605 			child.width = this.width;
8606 			child.height = this.height;
8607 			child.recomputeChildLayout();
8608 		} else
8609 		foreach(child; children)
8610 			child.recomputeChildLayout();
8611 	}
8612 
8613 	alias width = typeof(super).width;
8614 	alias height = typeof(super).height;
8615 
8616 	@property int width(int w) @nogc pure @safe nothrow {
8617 		return this._width = w;
8618 	}
8619 
8620 	@property int height(int w) @nogc pure @safe nothrow {
8621 		return this._height = w;
8622 	}
8623 
8624 }
8625 
8626 /++
8627 	FixedPosition is like [StaticPosition], but its coordinates
8628 	are always relative to the viewport, meaning they do not scroll with
8629 	the parent content.
8630 +/
8631 class FixedPosition : StaticPosition {
8632 	///
8633 	this(Widget parent) { super(parent); }
8634 }
8635 
8636 version(win32_widgets)
8637 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
8638 	if(true) {
8639 		// cmd == 0 = menu, cmd == 1 = accelerator
8640 		if(auto item = idm in Action.mapping) {
8641 			foreach(handler; (*item).triggered)
8642 				handler();
8643 		/*
8644 			auto event = new Event("triggered", *item);
8645 			event.button = idm;
8646 			event.dispatch();
8647 		*/
8648 			return 0;
8649 		}
8650 	}
8651 	if(handle)
8652 	if(auto widgetp = handle in Widget.nativeMapping) {
8653 		(*widgetp).handleWmCommand(cmd, idm);
8654 		return 0;
8655 	}
8656 	return 1;
8657 }
8658 
8659 
8660 ///
8661 class Window : Widget {
8662 	Widget[] mouseCapturedBy;
8663 	void captureMouse(Widget byWhom) {
8664 		assert(byWhom !is null);
8665 		if(mouseCapturedBy.length > 0) {
8666 			auto cc = mouseCapturedBy[$-1];
8667 			if(cc is byWhom)
8668 				return; // or should it throw?
8669 			auto par = byWhom;
8670 			while(par) {
8671 				if(cc is par)
8672 					goto allowed;
8673 				par = par.parent;
8674 			}
8675 
8676 			throw new Exception("mouse is already captured by other widget");
8677 		}
8678 		allowed:
8679 		mouseCapturedBy ~= byWhom;
8680 		if(mouseCapturedBy.length == 1)
8681 			win.grabInput(false, true, false);
8682 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
8683 	}
8684 	void releaseMouseCapture() {
8685 		if(mouseCapturedBy.length == 0)
8686 			return; // or should it throw?
8687 		mouseCapturedBy = mouseCapturedBy[0 .. $-1];
8688 		mouseCapturedBy.assumeSafeAppend();
8689 		if(mouseCapturedBy.length == 0)
8690 			win.releaseInputGrab();
8691 	}
8692 
8693 
8694 	/++
8695 
8696 	+/
8697 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8698 		return .messageBox(this, title, message, style, icon);
8699 	}
8700 
8701 	/// ditto
8702 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8703 		return messageBox(null, message, style, icon);
8704 	}
8705 
8706 
8707 	/++
8708 		Sets the window icon which is often seen in title bars and taskbars.
8709 
8710 		A future plan is to offer an overload that takes an array too for multiple sizes, but right now you should probably set 16x16 or 32x32 images here.
8711 
8712 		History:
8713 			Added April 5, 2022 (dub v10.8)
8714 	+/
8715 	@property void icon(MemoryImage icon) {
8716 		if(win && icon)
8717 			win.icon = icon;
8718 	}
8719 
8720 	// 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
8721 	// this does NOT change the icon on the window! That's what the other overload is for
8722 	static @property .icon icon(GenericIcons i) {
8723 		return .icon(i);
8724 	}
8725 
8726 	///
8727 	@scriptable
8728 	@property bool focused() {
8729 		return win.focused;
8730 	}
8731 
8732 	static class Style : Widget.Style {
8733 		override WidgetBackground background() {
8734 			version(custom_widgets)
8735 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8736 			else version(win32_widgets)
8737 				return WidgetBackground(Color.transparent);
8738 			else static assert(0);
8739 		}
8740 	}
8741 	mixin OverrideStyle!Style;
8742 
8743 	/++
8744 		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.
8745 	+/
8746 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8747 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8748 	}
8749 
8750 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8751 		OperatingSystemFont font;
8752 		if(auto vt = WidgetPainter.visualTheme) {
8753 			font = vt.defaultFontCached(96); // FIXME
8754 		}
8755 
8756 		if(font is null) {
8757 			static int defaultHeightCache;
8758 			if(defaultHeightCache == 0) {
8759 				font = new OperatingSystemFont;
8760 				font.loadDefault;
8761 				defaultHeightCache = font.height();// * 5 / 4;
8762 			}
8763 			return defaultHeightCache;
8764 		}
8765 
8766 		return font.height();// * 5 / 4;
8767 	}
8768 
8769 	Widget focusedWidget;
8770 
8771 	private SimpleWindow win_;
8772 
8773 	@property {
8774 		/++
8775 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8776 
8777 			History:
8778 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8779 		+/
8780 		public SimpleWindow win() {
8781 			return win_;
8782 		}
8783 		///
8784 		protected void win(SimpleWindow w) {
8785 			win_ = w;
8786 		}
8787 	}
8788 
8789 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8790 	this(Widget p) {
8791 		tabStop = false;
8792 		super(p);
8793 	}
8794 
8795 	private void actualRedraw() {
8796 		if(recomputeChildLayoutRequired)
8797 			recomputeChildLayoutEntry();
8798 		if(!showing) return;
8799 
8800 		assert(parentWindow !is null);
8801 
8802 		auto w = drawableWindow;
8803 		if(w is null)
8804 			w = parentWindow.win;
8805 
8806 		if(w.closed())
8807 			return;
8808 
8809 		auto ugh = this.parent;
8810 		int lox, loy;
8811 		while(ugh) {
8812 			lox += ugh.x;
8813 			loy += ugh.y;
8814 			ugh = ugh.parent;
8815 		}
8816 		auto painter = w.draw(true);
8817 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8818 	}
8819 
8820 
8821 	private bool skipNextChar = false;
8822 
8823 	/++
8824 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8825 
8826 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8827 	+/
8828 	this(SimpleWindow win) {
8829 
8830 		static if(UsingSimpledisplayX11) {
8831 			win.discardAdditionalConnectionState = &discardXConnectionState;
8832 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8833 		}
8834 
8835 		tabStop = false;
8836 		super(null);
8837 		this.win = win;
8838 
8839 		win.addEventListener((Widget.RedrawEvent) {
8840 			if(win.eventQueued!RecomputeEvent) {
8841 				// writeln("skipping");
8842 				return; // let the recompute event do the actual redraw
8843 			}
8844 			this.actualRedraw();
8845 		});
8846 
8847 		win.addEventListener((Widget.RecomputeEvent) {
8848 			recomputeChildLayoutEntry();
8849 			if(win.eventQueued!RedrawEvent)
8850 				return; // let the queued one do it
8851 			else {
8852 				// writeln("drawing");
8853 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8854 			}
8855 		});
8856 
8857 		this.width = win.width;
8858 		this.height = win.height;
8859 		this.parentWindow = this;
8860 
8861 		win.closeQuery = () {
8862 			if(this.emit!ClosingEvent())
8863 				win.close();
8864 		};
8865 		win.onClosing = () {
8866 			this.emit!ClosedEvent();
8867 		};
8868 
8869 		win.windowResized = (int w, int h) {
8870 			this.width = w;
8871 			this.height = h;
8872 			queueRecomputeChildLayout();
8873 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8874 			//version(win32_widgets)
8875 				//InvalidateRect(hwnd, null, true);
8876 			redraw();
8877 		};
8878 
8879 		win.onFocusChange = (bool getting) {
8880 			// sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString);
8881 			if(this.focusedWidget) {
8882 				if(getting) {
8883 					this.focusedWidget.emit!FocusEvent();
8884 					this.focusedWidget.emit!FocusInEvent();
8885 				} else {
8886 					this.focusedWidget.emit!BlurEvent();
8887 					this.focusedWidget.emit!FocusOutEvent();
8888 				}
8889 			}
8890 
8891 			if(getting) {
8892 				this.emit!FocusEvent();
8893 				this.emit!FocusInEvent();
8894 			} else {
8895 				this.emit!BlurEvent();
8896 				this.emit!FocusOutEvent();
8897 			}
8898 		};
8899 
8900 		win.onDpiChanged = {
8901 			this.queueRecomputeChildLayout();
8902 			auto event = new DpiChangedEvent(this);
8903 			event.sendDirectly();
8904 
8905 			privateDpiChanged();
8906 		};
8907 
8908 		win.setEventHandlers(
8909 			(MouseEvent e) {
8910 				dispatchMouseEvent(e);
8911 			},
8912 			(KeyEvent e) {
8913 				//writefln("%x   %s", cast(uint) e.key, e.key);
8914 				dispatchKeyEvent(e);
8915 			},
8916 			(dchar e) {
8917 				if(e == 13) e = 10; // hack?
8918 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
8919 				dispatchCharEvent(e);
8920 			},
8921 		);
8922 
8923 		addEventListener("char", (Widget, Event ev) {
8924 			if(skipNextChar) {
8925 				ev.preventDefault();
8926 				skipNextChar = false;
8927 			}
8928 		});
8929 
8930 		version(win32_widgets)
8931 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
8932 			if(hwnd !is this.win.impl.hwnd)
8933 				return 1; // we don't care... pass it on
8934 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
8935 			if(mustReturn)
8936 				return ret;
8937 			return 1; // pass it on
8938 		};
8939 
8940 		if(Window.newWindowCreated)
8941 			Window.newWindowCreated(this);
8942 	}
8943 
8944 	version(custom_widgets)
8945 	override void defaultEventHandler_click(ClickEvent event) {
8946 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
8947 			if(event.target && event.target.tabStop)
8948 				event.target.focus();
8949 		}
8950 	}
8951 
8952 	private static void delegate(Window) newWindowCreated;
8953 
8954 	version(win32_widgets)
8955 	override void paint(WidgetPainter painter) {
8956 		/*
8957 		RECT rect;
8958 		rect.right = this.width;
8959 		rect.bottom = this.height;
8960 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
8961 		*/
8962 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
8963 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
8964 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
8965 		// since the pen is null, to fill the whole space, we need the +1 on both.
8966 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
8967 		SelectObject(painter.impl.hdc, p);
8968 		SelectObject(painter.impl.hdc, b);
8969 	}
8970 	version(custom_widgets)
8971 	override void paint(WidgetPainter painter) {
8972 		auto cs = getComputedStyle();
8973 		painter.fillColor = cs.windowBackgroundColor;
8974 		painter.outlineColor = cs.windowBackgroundColor;
8975 		painter.drawRectangle(Point(0, 0), this.width, this.height);
8976 	}
8977 
8978 
8979 	override void defaultEventHandler_keydown(KeyDownEvent event) {
8980 		Widget _this = event.target;
8981 
8982 		if(event.key == Key.Tab) {
8983 			/* Window tab ordering is a recursive thingy with each group */
8984 
8985 			// FIXME inefficient
8986 			Widget[] helper(Widget p) {
8987 				if(p.hidden)
8988 					return null;
8989 				Widget[] childOrdering;
8990 
8991 				auto children = p.children.dup;
8992 
8993 				while(true) {
8994 					// UIs should be generally small, so gonna brute force it a little
8995 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
8996 
8997 					Widget smallestTab;
8998 					foreach(ref c; children) {
8999 						if(c is null) continue;
9000 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
9001 							smallestTab = c;
9002 							c = null;
9003 						}
9004 					}
9005 					if(smallestTab !is null) {
9006 						if(smallestTab.tabStop && !smallestTab.hidden)
9007 							childOrdering ~= smallestTab;
9008 						if(!smallestTab.hidden)
9009 							childOrdering ~= helper(smallestTab);
9010 					} else
9011 						break;
9012 
9013 				}
9014 
9015 				return childOrdering;
9016 			}
9017 
9018 			Widget[] tabOrdering = helper(this);
9019 
9020 			Widget recipient;
9021 
9022 			if(tabOrdering.length) {
9023 				bool seenThis = false;
9024 				Widget previous;
9025 				foreach(idx, child; tabOrdering) {
9026 					if(child is focusedWidget) {
9027 
9028 						if(event.shiftKey) {
9029 							if(idx == 0)
9030 								recipient = tabOrdering[$-1];
9031 							else
9032 								recipient = tabOrdering[idx - 1];
9033 							break;
9034 						}
9035 
9036 						seenThis = true;
9037 						if(idx + 1 == tabOrdering.length) {
9038 							// we're at the end, either move to the next group
9039 							// or start back over
9040 							recipient = tabOrdering[0];
9041 						}
9042 						continue;
9043 					}
9044 					if(seenThis) {
9045 						recipient = child;
9046 						break;
9047 					}
9048 					previous = child;
9049 				}
9050 			}
9051 
9052 			if(recipient !is null) {
9053 				//  writeln(typeid(recipient));
9054 				recipient.focus();
9055 
9056 				skipNextChar = true;
9057 			}
9058 		}
9059 
9060 		debug if(event.key == Key.F12) {
9061 			if(devTools) {
9062 				devTools.close();
9063 				devTools = null;
9064 			} else {
9065 				devTools = new DevToolWindow(this);
9066 				devTools.show();
9067 			}
9068 		}
9069 	}
9070 
9071 	debug DevToolWindow devTools;
9072 
9073 
9074 	/++
9075 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
9076 
9077 		History:
9078 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
9079 
9080 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
9081 	+/
9082 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9083 		if(title is null) {
9084 			import core.runtime;
9085 			if(Runtime.args.length)
9086 				title = Runtime.args[0];
9087 		}
9088 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
9089 
9090 		static if(UsingSimpledisplayX11)
9091 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
9092 		///+
9093 		// for input proxy
9094 		auto display = XDisplayConnection.get;
9095 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
9096 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
9097 		XMapWindow(display, inputProxy);
9098 		// writefln("input proxy: 0x%0x", inputProxy);
9099 		this.inputProxy = new SimpleWindow(inputProxy);
9100 
9101 		/+
9102 		this.inputProxy.onFocusChange = (bool getting) {
9103 			sdpyPrintDebugString("input proxy focus change ", getting);
9104 		};
9105 		+/
9106 
9107 		XEvent lastEvent;
9108 		this.inputProxy.handleNativeEvent = (XEvent ev) {
9109 			lastEvent = ev;
9110 			return 1;
9111 		};
9112 		this.inputProxy.setEventHandlers(
9113 			(MouseEvent e) {
9114 				dispatchMouseEvent(e);
9115 			},
9116 			(KeyEvent e) {
9117 				//writefln("%x   %s", cast(uint) e.key, e.key);
9118 				if(dispatchKeyEvent(e)) {
9119 					// FIXME: i should trap error
9120 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
9121 						auto thing = nw.focusableWindow();
9122 						if(thing && thing.window) {
9123 							lastEvent.xkey.window = thing.window;
9124 							// writeln("sending event ", lastEvent.xkey);
9125 							trapXErrors( {
9126 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
9127 							});
9128 						}
9129 					}
9130 				}
9131 			},
9132 			(dchar e) {
9133 				if(e == 13) e = 10; // hack?
9134 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9135 				dispatchCharEvent(e);
9136 			},
9137 		);
9138 
9139 		this.inputProxy.populateXic();
9140 		// done
9141 		//+/
9142 		}
9143 
9144 
9145 
9146 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
9147 
9148 		this(win);
9149 	}
9150 
9151 	SimpleWindow inputProxy;
9152 
9153 	private SimpleWindow setRequestedInputFocus() {
9154 		return inputProxy;
9155 	}
9156 
9157 	/// ditto
9158 	this(string title, int width = 500, int height = 500) {
9159 		this(width, height, title);
9160 	}
9161 
9162 	///
9163 	@property string title() { return parentWindow.win.title; }
9164 	///
9165 	@property void title(string title) { parentWindow.win.title = title; }
9166 
9167 	///
9168 	@scriptable
9169 	void close() {
9170 		win.close();
9171 		// I synchronize here upon window closing to ensure all child windows
9172 		// get updated too before the event loop. This avoids some random X errors.
9173 		static if(UsingSimpledisplayX11) {
9174 			runInGuiThread( {
9175 				XSync(XDisplayConnection.get, false);
9176 			});
9177 		}
9178 	}
9179 
9180 	bool dispatchKeyEvent(KeyEvent ev) {
9181 		auto wid = focusedWidget;
9182 		if(wid is null)
9183 			wid = this;
9184 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
9185 		event.originalKeyEvent = ev;
9186 		event.key = ev.key;
9187 		event.state = ev.modifierState;
9188 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9189 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9190 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9191 		event.dispatch();
9192 
9193 		return !event.propagationStopped;
9194 	}
9195 
9196 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
9197 	bool dispatchCharEvent(dchar ch) {
9198 		if(focusedWidget) {
9199 			auto event = new CharEvent(focusedWidget, ch);
9200 			event.dispatch();
9201 			return !event.propagationStopped;
9202 		}
9203 		return true;
9204 	}
9205 
9206 	Widget mouseLastOver;
9207 	Widget mouseLastDownOn;
9208 	bool lastWasDoubleClick;
9209 	bool dispatchMouseEvent(MouseEvent ev) {
9210 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
9211 		auto ele = eleR.widget;
9212 
9213 		auto captureEle = ele;
9214 
9215 		auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null;
9216 		if(mouseCapturedBy !is null) {
9217 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
9218 				captureEle = mouseCapturedBy;
9219 		}
9220 
9221 		// a hack to get it relative to the widget.
9222 		eleR.x = ev.x;
9223 		eleR.y = ev.y;
9224 		auto pain = captureEle;
9225 		while(pain) {
9226 			eleR.x -= pain.x;
9227 			eleR.y -= pain.y;
9228 			pain.addScrollPosition(eleR.x, eleR.y);
9229 			pain = pain.parent;
9230 		}
9231 
9232 		void populateMouseEventBase(MouseEventBase event) {
9233 			event.button = ev.button;
9234 			event.buttonLinear = ev.buttonLinear;
9235 			event.state = ev.modifierState;
9236 			event.clientX = eleR.x;
9237 			event.clientY = eleR.y;
9238 
9239 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9240 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9241 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9242 		}
9243 
9244 		if(ev.type == MouseEventType.buttonPressed) {
9245 			{
9246 				auto event = new MouseDownEvent(captureEle);
9247 				populateMouseEventBase(event);
9248 				event.dispatch();
9249 			}
9250 
9251 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
9252 				auto event = new DoubleClickEvent(captureEle);
9253 				populateMouseEventBase(event);
9254 				event.dispatch();
9255 				lastWasDoubleClick = ev.doubleClick;
9256 			} else {
9257 				lastWasDoubleClick = false;
9258 			}
9259 
9260 			mouseLastDownOn = ele;
9261 		} else if(ev.type == MouseEventType.buttonReleased) {
9262 			{
9263 				auto event = new MouseUpEvent(captureEle);
9264 				populateMouseEventBase(event);
9265 				event.dispatch();
9266 			}
9267 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
9268 				auto event = new ClickEvent(captureEle);
9269 				populateMouseEventBase(event);
9270 				event.dispatch();
9271 			}
9272 		} else if(ev.type == MouseEventType.motion) {
9273 			// motion
9274 			{
9275 				auto event = new MouseMoveEvent(captureEle);
9276 				populateMouseEventBase(event); // fills in button which is meaningless but meh
9277 				event.dispatch();
9278 			}
9279 
9280 			if(mouseLastOver !is ele) {
9281 				if(ele !is null) {
9282 					if(!isAParentOf(ele, mouseLastOver)) {
9283 						ele.setDynamicState(DynamicState.hover, true);
9284 						auto event = new MouseEnterEvent(ele);
9285 						event.relatedTarget = mouseLastOver;
9286 						event.sendDirectly();
9287 
9288 						ele.useStyleProperties((scope Widget.Style s) {
9289 							ele.parentWindow.win.cursor = s.cursor;
9290 						});
9291 					}
9292 				}
9293 
9294 				if(mouseLastOver !is null) {
9295 					if(!isAParentOf(mouseLastOver, ele)) {
9296 						mouseLastOver.setDynamicState(DynamicState.hover, false);
9297 						auto event = new MouseLeaveEvent(mouseLastOver);
9298 						event.relatedTarget = ele;
9299 						event.sendDirectly();
9300 					}
9301 				}
9302 
9303 				if(ele !is null) {
9304 					auto event = new MouseOverEvent(ele);
9305 					event.relatedTarget = mouseLastOver;
9306 					event.dispatch();
9307 				}
9308 
9309 				if(mouseLastOver !is null) {
9310 					auto event = new MouseOutEvent(mouseLastOver);
9311 					event.relatedTarget = ele;
9312 					event.dispatch();
9313 				}
9314 
9315 				mouseLastOver = ele;
9316 			}
9317 		}
9318 
9319 		return true; // FIXME: the event default prevented?
9320 	}
9321 
9322 	/++
9323 		Shows the window and runs the application event loop.
9324 
9325 		Blocks until this window is closed.
9326 
9327 		Bugs:
9328 
9329 		$(PITFALL
9330 			You should always have one event loop live for your application.
9331 			If you make two windows in sequence, the second call to loop (or
9332 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
9333 			might fail:
9334 
9335 			---
9336 			// don't do this!
9337 			auto window = new Window();
9338 			window.loop();
9339 
9340 			// or new Window or new MainWindow, all the same
9341 			auto window2 = new SimpleWindow();
9342 			window2.eventLoop(0); // problematic! might crash
9343 			---
9344 
9345 			simpledisplay's current implementation assumes that final cleanup is
9346 			done when the event loop refcount reaches zero. So after the first
9347 			eventLoop returns, when there isn't already another one active, it assumes
9348 			the program will exit soon and cleans up.
9349 
9350 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
9351 			it eventually, but in the mean time, there's an easy solution:
9352 
9353 			---
9354 			// do this
9355 			EventLoop mainEventLoop = EventLoop.get; // just add this line
9356 
9357 			auto window = new Window();
9358 			window.loop();
9359 
9360 			// or any other type of Window etc.
9361 			auto window2 = new Window();
9362 			window2.loop(); // perfectly fine since mainEventLoop still alive
9363 			---
9364 
9365 			By adding a top-level reference to the event loop, it ensures the final cleanup
9366 			is not performed until it goes out of scope too, letting the individual window loops
9367 			work without trouble despite the bug.
9368 		)
9369 
9370 		History:
9371 			The [BlockingMode] parameter was added on December 8, 2021.
9372 			The default behavior is to block until the application quits
9373 			(so all windows have been closed), unless another minigui or
9374 			simpledisplay event loop is already running, in which case it
9375 			will block until this window closes specifically.
9376 	+/
9377 	@scriptable
9378 	void loop(BlockingMode bm = BlockingMode.automatic) {
9379 		if(win.closed)
9380 			return; // otherwise show will throw
9381 		show();
9382 		win.eventLoopWithBlockingMode(bm, 0);
9383 	}
9384 
9385 	private bool firstShow = true;
9386 
9387 	@scriptable
9388 	override void show() {
9389 		bool rd = false;
9390 		if(firstShow) {
9391 			firstShow = false;
9392 			queueRecomputeChildLayout();
9393 			// unless the programmer already called focus on something, pick something ourselves
9394 			auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus?
9395 			if(f)
9396 				f.focus();
9397 			redraw();
9398 		}
9399 		win.show();
9400 		super.show();
9401 	}
9402 	@scriptable
9403 	override void hide() {
9404 		win.hide();
9405 		super.hide();
9406 	}
9407 
9408 	static Widget getFirstFocusable(Widget start) {
9409 		if(start is null)
9410 			return null;
9411 
9412 		foreach(widget; &start.focusableWidgets) {
9413 			return widget;
9414 		}
9415 
9416 		return null;
9417 	}
9418 
9419 	static Widget getLastFocusable(Widget start) {
9420 		if(start is null)
9421 			return null;
9422 
9423 		Widget last;
9424 		foreach(widget; &start.focusableWidgets) {
9425 			last = widget;
9426 		}
9427 
9428 		return last;
9429 	}
9430 
9431 
9432 	mixin Emits!ClosingEvent;
9433 	mixin Emits!ClosedEvent;
9434 }
9435 
9436 /++
9437 	History:
9438 		Added January 12, 2022
9439 +/
9440 class DpiChangedEvent : Event {
9441 	enum EventString = "dpichanged";
9442 
9443 	this(Widget target) {
9444 		super(EventString, target);
9445 	}
9446 }
9447 
9448 debug private class DevToolWindow : Window {
9449 	Window p;
9450 
9451 	TextEdit parentList;
9452 	TextEdit logWindow;
9453 	TextLabel clickX, clickY;
9454 
9455 	this(Window p) {
9456 		this.p = p;
9457 		super(400, 300, "Developer Toolbox");
9458 
9459 		logWindow = new TextEdit(this);
9460 		parentList = new TextEdit(this);
9461 
9462 		auto hl = new HorizontalLayout(this);
9463 		clickX = new TextLabel("", TextAlignment.Right, hl);
9464 		clickY = new TextLabel("", TextAlignment.Right, hl);
9465 
9466 		parentListeners ~= p.addEventListener("*", (Event ev) {
9467 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
9468 		});
9469 
9470 		parentListeners ~= p.addEventListener((ClickEvent ev) {
9471 			auto s = ev.srcElement;
9472 
9473 			string list;
9474 
9475 			void addInfo(Widget s) {
9476 				list ~= s.toString();
9477 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
9478 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
9479 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
9480 				list ~= "\n\theight: " ~ toInternal!string(s.height);
9481 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
9482 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
9483 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
9484 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
9485 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
9486 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
9487 			}
9488 
9489 			addInfo(s);
9490 
9491 			s = s.parent;
9492 			while(s) {
9493 				list ~= "\n";
9494 				addInfo(s);
9495 				s = s.parent;
9496 			}
9497 			parentList.content = list;
9498 
9499 			clickX.label = toInternal!string(ev.clientX);
9500 			clickY.label = toInternal!string(ev.clientY);
9501 		});
9502 	}
9503 
9504 	EventListener[] parentListeners;
9505 
9506 	override void close() {
9507 		assert(p !is null);
9508 		foreach(p; parentListeners)
9509 			p.disconnect();
9510 		parentListeners = null;
9511 		p.devTools = null;
9512 		p = null;
9513 		super.close();
9514 	}
9515 
9516 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
9517 		if(ev.key == Key.F12) {
9518 			this.close();
9519 			if(p)
9520 				p.devTools = null;
9521 		} else {
9522 			super.defaultEventHandler_keydown(ev);
9523 		}
9524 	}
9525 
9526 	void log(T...)(T t) {
9527 		string str;
9528 		import std.conv;
9529 		foreach(i; t)
9530 			str ~= to!string(i);
9531 		str ~= "\n";
9532 		logWindow.addText(str);
9533 		logWindow.scrollToBottom();
9534 
9535 		//version(custom_widgets)
9536 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
9537 	}
9538 }
9539 
9540 /++
9541 	A dialog is a transient window that intends to get information from
9542 	the user before being dismissed.
9543 +/
9544 class Dialog : Window {
9545 	///
9546 	this(Window parent, int width, int height, string title = null) {
9547 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
9548 
9549 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9550 	}
9551 
9552 	///
9553 	this(Window parent, string title, int width, int height) {
9554 		this(parent, width, height, title);
9555 	}
9556 
9557 	deprecated("Pass an explicit parent window, even if it is `null`")
9558 	this(int width, int height, string title = null) {
9559 		this(null, width, height, title);
9560 	}
9561 
9562 	///
9563 	void OK() {
9564 
9565 	}
9566 
9567 	///
9568 	void Cancel() {
9569 		this.close();
9570 	}
9571 }
9572 
9573 /++
9574 	A custom widget similar to the HTML5 <details> tag.
9575 +/
9576 version(none)
9577 class DetailsView : Widget {
9578 
9579 }
9580 
9581 // FIXME: maybe i should expose the other list views Windows offers too
9582 
9583 /++
9584 	A TableView is a widget made to display a table of data strings.
9585 
9586 
9587 	Future_Directions:
9588 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
9589 
9590 		I will add a selection changed event at some point, as well as item clicked events.
9591 	History:
9592 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
9593 	See_Also:
9594 		[ListWidget] which displays a list of strings without additional columns.
9595 +/
9596 class TableView : Widget {
9597 	/++
9598 
9599 	+/
9600 	this(Widget parent) {
9601 		super(parent);
9602 
9603 		version(win32_widgets) {
9604 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
9605 		} else version(custom_widgets) {
9606 			auto smw = new ScrollMessageWidget(this);
9607 			smw.addDefaultKeyboardListeners();
9608 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
9609 			tvwi = new TableViewWidgetInner(this, smw);
9610 		}
9611 	}
9612 
9613 	// FIXME: auto-size columns on double click of header thing like in Windows
9614 	// it need only make the currently displayed things fit well.
9615 
9616 
9617 	private ColumnInfo[] columns;
9618 	private int itemCount;
9619 
9620 	version(custom_widgets) private {
9621 		TableViewWidgetInner tvwi;
9622 	}
9623 
9624 	/// Passed to [setColumnInfo]
9625 	static struct ColumnInfo {
9626 		const(char)[] name; /// the name displayed in the header
9627 		/++
9628 			The default width, in pixels. As a special case, you can set this to -1
9629 			if you want the system to try to automatically size the width to fit visible
9630 			content. If it can't, it will try to pick a sensible default size.
9631 
9632 			Any other negative value is not allowed and may lead to unpredictable results.
9633 
9634 			History:
9635 				The -1 behavior was specified on December 3, 2021. It actually worked before
9636 				anyway on Win32 but now it is a formal feature with partial Linux support.
9637 
9638 			Bugs:
9639 				It doesn't actually attempt to calculate a best-fit width on Linux as of
9640 				December 3, 2021. I do plan to fix this in the future, but Windows is the
9641 				priority right now. At least it doesn't break things when you use it now.
9642 		+/
9643 		int width;
9644 
9645 		/++
9646 			Alignment of the text in the cell. Applies to the header as well as all data in this
9647 			column.
9648 
9649 			Bugs:
9650 				On Windows, the first column ignores this member and is always left aligned.
9651 				You can work around this by inserting a dummy first column with width = 0
9652 				then putting your actual data in the second column, which does respect the
9653 				alignment.
9654 
9655 				This is a quirk of the operating system's implementation going back a very
9656 				long time and is unlikely to ever be fixed.
9657 		+/
9658 		TextAlignment alignment;
9659 
9660 		/++
9661 			After all the pixel widths have been assigned, any left over
9662 			space is divided up among all columns and distributed to according
9663 			to the widthPercent field.
9664 
9665 
9666 			For example, if you have two fields, both with width 50 and one with
9667 			widthPercent of 25 and the other with widthPercent of 75, and the
9668 			container is 200 pixels wide, first both get their width of 50.
9669 			then the 100 remaining pixels are split up, so the one gets a total
9670 			of 75 pixels and the other gets a total of 125.
9671 
9672 			This is automatically applied as the window is resized.
9673 
9674 			If there is not enough space - that is, when a horizontal scrollbar
9675 			needs to appear - there are 0 pixels divided up, and thus everyone
9676 			gets 0. This can cause a column to shrink out of proportion when
9677 			passing the scroll threshold.
9678 
9679 			It is important to still set a fixed width (that is, to populate the
9680 			`width` field) even if you use the percents because that will be the
9681 			default minimum in the event of a scroll bar appearing.
9682 
9683 			The percents total in the column can never exceed 100 or be less than 0.
9684 			Doing this will trigger an assert error.
9685 
9686 			Implementation note:
9687 
9688 			Please note that percentages are only recalculated 1) upon original
9689 			construction and 2) upon resizing the control. If the user adjusts the
9690 			width of a column, the percentage items will not be updated.
9691 
9692 			On the other hand, if the user adjusts the width of a percentage column
9693 			then resizes the window, it is recalculated, meaning their hand adjustment
9694 			is discarded. This specific behavior may change in the future as it is
9695 			arguably a bug, but I'm not certain yet.
9696 
9697 			History:
9698 				Added November 10, 2021 (dub v10.4)
9699 		+/
9700 		int widthPercent;
9701 
9702 
9703 		private int calculatedWidth;
9704 	}
9705 	/++
9706 		Sets the number of columns along with information about the headers.
9707 
9708 		Please note: on Windows, the first column ignores your alignment preference
9709 		and is always left aligned.
9710 	+/
9711 	void setColumnInfo(ColumnInfo[] columns...) {
9712 
9713 		foreach(ref c; columns) {
9714 			c.name = c.name.idup;
9715 		}
9716 		this.columns = columns.dup;
9717 
9718 		updateCalculatedWidth(false);
9719 
9720 		version(custom_widgets) {
9721 			tvwi.header.updateHeaders();
9722 			tvwi.updateScrolls();
9723 		} else version(win32_widgets)
9724 		foreach(i, column; this.columns) {
9725 			LVCOLUMN lvColumn;
9726 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
9727 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
9728 
9729 			auto bfr = WCharzBuffer(column.name);
9730 			lvColumn.pszText = bfr.ptr;
9731 
9732 			if(column.alignment & TextAlignment.Center)
9733 				lvColumn.fmt = LVCFMT_CENTER;
9734 			else if(column.alignment & TextAlignment.Right)
9735 				lvColumn.fmt = LVCFMT_RIGHT;
9736 			else
9737 				lvColumn.fmt = LVCFMT_LEFT;
9738 
9739 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
9740 				throw new WindowsApiException("Insert Column Fail", GetLastError());
9741 		}
9742 	}
9743 
9744 	private int getActualSetSize(size_t i, bool askWindows) {
9745 		version(win32_widgets)
9746 			if(askWindows)
9747 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
9748 		auto w = columns[i].width;
9749 		if(w == -1)
9750 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9751 		return w;
9752 	}
9753 
9754 	private void updateCalculatedWidth(bool informWindows) {
9755 		int padding;
9756 		version(win32_widgets)
9757 			padding = 4;
9758 		int remaining = this.width;
9759 		foreach(i, column; columns)
9760 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9761 		remaining -= padding;
9762 		if(remaining < 0)
9763 			remaining = 0;
9764 
9765 		int percentTotal;
9766 		foreach(i, ref column; columns) {
9767 			percentTotal += column.widthPercent;
9768 
9769 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9770 
9771 			column.calculatedWidth = c;
9772 
9773 			version(win32_widgets)
9774 			if(informWindows)
9775 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9776 		}
9777 
9778 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9779 		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).");
9780 
9781 
9782 	}
9783 
9784 	override void registerMovement() {
9785 		super.registerMovement();
9786 
9787 		updateCalculatedWidth(true);
9788 	}
9789 
9790 	/++
9791 		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.
9792 	+/
9793 	void setItemCount(int count) {
9794 		this.itemCount = count;
9795 		version(custom_widgets) {
9796 			tvwi.updateScrolls();
9797 			redraw();
9798 		} else version(win32_widgets) {
9799 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9800 		}
9801 	}
9802 
9803 	/++
9804 		Clears all items;
9805 	+/
9806 	void clear() {
9807 		this.itemCount = 0;
9808 		this.columns = null;
9809 		version(custom_widgets) {
9810 			tvwi.header.updateHeaders();
9811 			tvwi.updateScrolls();
9812 			redraw();
9813 		} else version(win32_widgets) {
9814 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9815 		}
9816 	}
9817 
9818 	/+
9819 	version(win32_widgets)
9820 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9821 		auto itemId = dis.itemID;
9822 		auto hdc = dis.hDC;
9823 		auto rect = dis.rcItem;
9824 		switch(dis.itemAction) {
9825 			case ODA_DRAWENTIRE:
9826 
9827 				// FIXME: do other items
9828 				// FIXME: do the focus rectangle i guess
9829 				// FIXME: alignment
9830 				// FIXME: column width
9831 				// FIXME: padding left
9832 				// FIXME: check dpi scaling
9833 				// FIXME: don't owner draw unless it is necessary.
9834 
9835 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9836 				RECT itemRect;
9837 				itemRect.top = 1; // subitem idx, 1-based
9838 				itemRect.left = LVIR_BOUNDS;
9839 
9840 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9841 				itemRect.left += padding;
9842 
9843 				getData(itemId, 0, (in char[] data) {
9844 					auto wdata = WCharzBuffer(data);
9845 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9846 
9847 				});
9848 			goto case;
9849 			case ODA_FOCUS:
9850 				if(dis.itemState & ODS_FOCUS)
9851 					DrawFocusRect(hdc, &rect);
9852 			break;
9853 			case ODA_SELECT:
9854 				// itemState & ODS_SELECTED
9855 			break;
9856 			default:
9857 		}
9858 		return 1;
9859 	}
9860 	+/
9861 
9862 	version(win32_widgets) {
9863 		CellStyle last;
9864 		COLORREF defaultColor;
9865 		COLORREF defaultBackground;
9866 	}
9867 
9868 	version(win32_widgets)
9869 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9870 		switch(code) {
9871 			case NM_CUSTOMDRAW:
9872 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9873 				switch(s.nmcd.dwDrawStage) {
9874 					case CDDS_PREPAINT:
9875 						if(getCellStyle is null)
9876 							return 0;
9877 
9878 						mustReturn = true;
9879 						return CDRF_NOTIFYITEMDRAW;
9880 					case CDDS_ITEMPREPAINT:
9881 						mustReturn = true;
9882 						return CDRF_NOTIFYSUBITEMDRAW;
9883 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9884 						mustReturn = true;
9885 
9886 						if(getCellStyle is null) // this SHOULD never happen...
9887 							return 0;
9888 
9889 						if(s.iSubItem == 0) {
9890 							// Windows resets it per row so we'll use item 0 as a chance
9891 							// to capture these for later
9892 							defaultColor = s.clrText;
9893 							defaultBackground = s.clrTextBk;
9894 						}
9895 
9896 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9897 						// if no special style and no reset needed...
9898 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9899 							return 0; // allow default processing to continue
9900 
9901 						last = style;
9902 
9903 						// might still need to reset or use the preference.
9904 
9905 						if(style.flags & CellStyle.Flags.textColorSet)
9906 							s.clrText = style.textColor.asWindowsColorRef;
9907 						else
9908 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
9909 						if(style.flags & CellStyle.Flags.backgroundColorSet)
9910 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
9911 						else
9912 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
9913 
9914 						return CDRF_NEWFONT;
9915 					default:
9916 						return 0;
9917 
9918 				}
9919 			case NM_RETURN: // no need since i subclass keydown
9920 			break;
9921 			case LVN_COLUMNCLICK:
9922 				auto info = cast(LPNMLISTVIEW) hdr;
9923 				this.emit!HeaderClickedEvent(info.iSubItem);
9924 			break;
9925 			case NM_CLICK:
9926 			case NM_DBLCLK:
9927 			case NM_RCLICK:
9928 			case NM_RDBLCLK:
9929 				// the item/subitem is set here and that can be a useful notification
9930 				// even beyond the normal click notification
9931 			break;
9932 			case LVN_GETDISPINFO:
9933 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
9934 				if(info.item.mask & LVIF_TEXT) {
9935 					if(getData) {
9936 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
9937 							auto bfr = WCharzBuffer(dataReceived);
9938 							auto len = info.item.cchTextMax;
9939 							if(bfr.length < len)
9940 								len = cast(typeof(len)) bfr.length;
9941 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
9942 							info.item.pszText[len] = 0;
9943 						});
9944 					} else {
9945 						info.item.pszText[0] = 0;
9946 					}
9947 					//info.item.iItem
9948 					//if(info.item.iSubItem)
9949 				}
9950 			break;
9951 			default:
9952 		}
9953 		return 0;
9954 	}
9955 
9956 	override bool encapsulatedChildren() {
9957 		return true;
9958 	}
9959 
9960 	/++
9961 		Informs the control that content has changed.
9962 
9963 		History:
9964 			Added November 10, 2021 (dub v10.4)
9965 	+/
9966 	void update() {
9967 		version(custom_widgets)
9968 			redraw();
9969 		else {
9970 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
9971 			UpdateWindow(hwnd);
9972 		}
9973 
9974 
9975 	}
9976 
9977 	/++
9978 		Called by the system to request the text content of an individual cell. You
9979 		should pass the text into the provided `sink` delegate. This function will be
9980 		called for each visible cell as-needed when drawing.
9981 	+/
9982 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
9983 
9984 	/++
9985 		Available per-cell style customization options. Use one of the constructors
9986 		provided to set the values conveniently, or default construct it and set individual
9987 		values yourself. Just remember to set the `flags` so your values are actually used.
9988 		If the flag isn't set, the field is ignored and the system default is used instead.
9989 
9990 		This is returned by the [getCellStyle] delegate.
9991 
9992 		Examples:
9993 			---
9994 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
9995 			auto table = new TableView(window);
9996 			// snip: you would set up columns here
9997 
9998 			// this is how you provide data to the table view class
9999 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
10000 				import std.conv;
10001 				sink(to!string(my_data[row][column]));
10002 			};
10003 
10004 			// and this is how you customize the colors
10005 			table.getCellStyle = delegate(int row, int column) {
10006 				return (my_data[row][column] < 0) ?
10007 					TableView.CellStyle(Color.red); // make negative numbers red
10008 					: TableView.CellStyle.init; // leave the rest alone
10009 			};
10010 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
10011 			---
10012 
10013 		History:
10014 			Added November 27, 2021 (dub v10.4)
10015 	+/
10016 	struct CellStyle {
10017 		/// 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.
10018 		this(Color textColor) {
10019 			this.textColor = textColor;
10020 			this.flags |= Flags.textColorSet;
10021 		}
10022 		/// Sets a custom text and background color.
10023 		this(Color textColor, Color backgroundColor) {
10024 			this.textColor = textColor;
10025 			this.backgroundColor = backgroundColor;
10026 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
10027 		}
10028 
10029 		Color textColor;
10030 		Color backgroundColor;
10031 		int flags; /// bitmask of [Flags]
10032 		/// available options to combine into [flags]
10033 		enum Flags {
10034 			textColorSet = 1 << 0,
10035 			backgroundColorSet = 1 << 1,
10036 		}
10037 	}
10038 	/++
10039 		Companion delegate to [getData] that allows you to custom style each
10040 		cell of the table.
10041 
10042 		Returns:
10043 			A [CellStyle] structure that describes the desired style for the
10044 			given cell. `return CellStyle.init` if you want the default style.
10045 
10046 		History:
10047 			Added November 27, 2021 (dub v10.4)
10048 	+/
10049 	CellStyle delegate(int row, int column) getCellStyle;
10050 
10051 	// i want to be able to do things like draw little colored things to show red for negative numbers
10052 	// or background color indicators or even in-cell charts
10053 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
10054 
10055 	/++
10056 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
10057 	+/
10058 	mixin Emits!HeaderClickedEvent;
10059 }
10060 
10061 /++
10062 	This is emitted by the [TableView] when a user clicks on a column header.
10063 
10064 	Its member `columnIndex` has the zero-based index of the column that was clicked.
10065 
10066 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
10067 
10068 	History:
10069 		Added November 27, 2021 (dub v10.4)
10070 +/
10071 class HeaderClickedEvent : Event {
10072 	enum EventString = "HeaderClicked";
10073 	this(Widget target, int columnIndex) {
10074 		this.columnIndex = columnIndex;
10075 		super(EventString, target);
10076 	}
10077 
10078 	/// The index of the column
10079 	int columnIndex;
10080 
10081 	///
10082 	override @property int intValue() {
10083 		return columnIndex;
10084 	}
10085 }
10086 
10087 version(custom_widgets)
10088 private class TableViewWidgetInner : Widget {
10089 
10090 // wrap this thing in a ScrollMessageWidget
10091 
10092 	TableView tvw;
10093 	ScrollMessageWidget smw;
10094 	HeaderWidget header;
10095 
10096 	this(TableView tvw, ScrollMessageWidget smw) {
10097 		this.tvw = tvw;
10098 		this.smw = smw;
10099 		super(smw);
10100 
10101 		this.tabStop = true;
10102 
10103 		header = new HeaderWidget(this, smw.getHeader());
10104 
10105 		smw.addEventListener("scroll", () {
10106 			this.redraw();
10107 			header.redraw();
10108 		});
10109 
10110 
10111 		// I need headers outside the scroll area but rendered on the same line as the up arrow
10112 		// FIXME: add a fixed header to the SMW
10113 	}
10114 
10115 	enum padding = 3;
10116 
10117 	void updateScrolls() {
10118 		int w;
10119 		foreach(idx, column; tvw.columns) {
10120 			if(column.width == 0) continue;
10121 			w += tvw.getActualSetSize(idx, false);// + padding;
10122 		}
10123 		smw.setTotalArea(w, tvw.itemCount);
10124 		columnsWidth = w;
10125 	}
10126 
10127 	private int columnsWidth;
10128 
10129 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
10130 
10131 	override void registerMovement() {
10132 		super.registerMovement();
10133 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
10134 		smw.setViewableArea(this.width, this.height / lh);
10135 	}
10136 
10137 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
10138 		int x;
10139 		int y;
10140 
10141 		int row = smw.position.y;
10142 
10143 		foreach(lol; 0 .. this.height / lh) {
10144 			if(row >= tvw.itemCount)
10145 				break;
10146 			x = 0;
10147 			foreach(columnNumber, column; tvw.columns) {
10148 				auto x2 = x + column.calculatedWidth;
10149 				auto smwx = smw.position.x;
10150 
10151 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
10152 					auto startX = x;
10153 					auto endX = x + column.calculatedWidth;
10154 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
10155 						case TextAlignment.Left: startX += padding; break;
10156 						case TextAlignment.Center: startX += padding; endX -= padding; break;
10157 						case TextAlignment.Right: endX -= padding; break;
10158 						default: /* broken */ break;
10159 					}
10160 					if(column.width != 0) // no point drawing an invisible column
10161 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
10162 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh)));
10163 
10164 						void dotext(WidgetPainter painter) {
10165 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
10166 						}
10167 
10168 						if(tvw.getCellStyle !is null) {
10169 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
10170 
10171 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
10172 								auto tempPainter = painter;
10173 								tempPainter.fillColor = style.backgroundColor;
10174 								tempPainter.outlineColor = style.backgroundColor;
10175 
10176 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
10177 									Point(endX - smw.position.x, y + lh));
10178 							}
10179 							auto tempPainter = painter;
10180 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
10181 								tempPainter.outlineColor = style.textColor;
10182 
10183 							dotext(tempPainter);
10184 						} else {
10185 							dotext(painter);
10186 						}
10187 					});
10188 				}
10189 
10190 				x += column.calculatedWidth;
10191 			}
10192 			row++;
10193 			y += lh;
10194 		}
10195 		return bounds;
10196 	}
10197 
10198 	static class Style : Widget.Style {
10199 		override WidgetBackground background() {
10200 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
10201 		}
10202 	}
10203 	mixin OverrideStyle!Style;
10204 
10205 	private static class HeaderWidget : Widget {
10206 		/+
10207 			maybe i should do a splitter thing on top of the other widgets
10208 			so the splitter itself isn't really drawn but still replies to mouse events?
10209 		+/
10210 		this(TableViewWidgetInner tvw, Widget parent) {
10211 			super(parent);
10212 			this.tvw = tvw;
10213 
10214 			this.remainder = new Button("", this);
10215 
10216 			this.addEventListener((scope ClickEvent ev) {
10217 				int header = -1;
10218 				foreach(idx, child; this.children[1 .. $]) {
10219 					if(child is ev.target) {
10220 						header = cast(int) idx;
10221 						break;
10222 					}
10223 				}
10224 
10225 				if(header != -1) {
10226 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
10227 					hce.dispatch();
10228 				}
10229 
10230 			});
10231 		}
10232 
10233 		void updateHeaders() {
10234 			foreach(child; children[1 .. $])
10235 				child.removeWidget();
10236 
10237 			foreach(column; tvw.tvw.columns) {
10238 				// the cast is ok because I dup it above, just the type is never changed.
10239 				// all this is private so it should never get messed up.
10240 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
10241 			}
10242 		}
10243 
10244 		Button remainder;
10245 		TableViewWidgetInner tvw;
10246 
10247 		override void recomputeChildLayout() {
10248 			registerMovement();
10249 			int pos;
10250 			foreach(idx, child; children[1 .. $]) {
10251 				if(idx >= tvw.tvw.columns.length)
10252 					continue;
10253 				child.x = pos;
10254 				child.y = 0;
10255 				child.width = tvw.tvw.columns[idx].calculatedWidth;
10256 				child.height = scaleWithDpi(16);// this.height;
10257 				pos += child.width;
10258 
10259 				child.recomputeChildLayout();
10260 			}
10261 
10262 			if(remainder is null)
10263 				return;
10264 
10265 			remainder.x = pos;
10266 			remainder.y = 0;
10267 			if(pos < this.width)
10268 				remainder.width = this.width - pos;// + 4;
10269 			else
10270 				remainder.width = 0;
10271 			remainder.height = scaleWithDpi(16);
10272 
10273 			remainder.recomputeChildLayout();
10274 		}
10275 
10276 		// for the scrollable children mixin
10277 		Point scrollOrigin() {
10278 			return Point(tvw.smw.position.x, 0);
10279 		}
10280 		void paintFrameAndBackground(WidgetPainter painter) { }
10281 
10282 		mixin ScrollableChildren;
10283 	}
10284 }
10285 
10286 /+
10287 
10288 // given struct / array / number / string / etc, make it viewable and editable
10289 class DataViewerWidget : Widget {
10290 
10291 }
10292 +/
10293 
10294 /++
10295 	A line edit box with an associated label.
10296 
10297 	History:
10298 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
10299 
10300 		```
10301 		Old: ________
10302 
10303 		New:
10304 		____________
10305 		```
10306 
10307 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
10308 
10309 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
10310 		horizontal label but left aligned. You may also consider a [GridLayout].
10311 +/
10312 alias LabeledLineEdit = Labeled!LineEdit;
10313 
10314 private int widthThatWouldFitChildLabels(Widget w) {
10315 	if(w is null)
10316 		return 0;
10317 
10318 	int max;
10319 
10320 	if(auto label = cast(TextLabel) w) {
10321 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
10322 	} else {
10323 		foreach(child; w.children) {
10324 			max = mymax(max, widthThatWouldFitChildLabels(child));
10325 		}
10326 	}
10327 
10328 	return max;
10329 }
10330 
10331 /++
10332 	History:
10333 		Added May 19, 2021
10334 +/
10335 class Labeled(T) : Widget {
10336 	///
10337 	this(string label, Widget parent) {
10338 		super(parent);
10339 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
10340 	}
10341 
10342 	/++
10343 		History:
10344 			The alignment parameter was added May 17, 2021
10345 	+/
10346 	this(string label, TextAlignment alignment, Widget parent) {
10347 		super(parent);
10348 		initialize!HorizontalLayout(label, alignment, parent);
10349 	}
10350 
10351 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
10352 		tabStop = false;
10353 		horizontal = is(L == HorizontalLayout);
10354 		auto hl = new L(this);
10355 		if(horizontal) {
10356 			static class SpecialTextLabel : TextLabel {
10357 				Widget outerParent;
10358 
10359 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
10360 					this.outerParent = outerParent;
10361 					super(label, alignment, parent);
10362 				}
10363 
10364 				override int flexBasisWidth() {
10365 					return widthThatWouldFitChildLabels(outerParent);
10366 				}
10367 				/+
10368 				override int widthShrinkiness() { return 0; }
10369 				override int widthStretchiness() { return 1; }
10370 				+/
10371 
10372 				override int paddingRight() { return 6; }
10373 				override int paddingLeft() { return 9; }
10374 
10375 				override int paddingTop() { return 3; }
10376 			}
10377 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
10378 		} else
10379 			this.label = new TextLabel(label, alignment, hl);
10380 		this.lineEdit = new T(hl);
10381 
10382 		this.label.labelFor = this.lineEdit;
10383 	}
10384 
10385 	private bool horizontal;
10386 
10387 	TextLabel label; ///
10388 	T lineEdit; ///
10389 
10390 	override int flexBasisWidth() { return 250; }
10391 	override int widthShrinkiness() { return 1; }
10392 
10393 	override int minHeight() {
10394 		return this.children[0].minHeight;
10395 	}
10396 	override int maxHeight() { return minHeight(); }
10397 	override int marginTop() { return 4; }
10398 	override int marginBottom() { return 4; }
10399 
10400 	// FIXME: i should prolly call it value as well as content tbh
10401 
10402 	///
10403 	@property string content() {
10404 		return lineEdit.content;
10405 	}
10406 	///
10407 	@property void content(string c) {
10408 		return lineEdit.content(c);
10409 	}
10410 
10411 	///
10412 	void selectAll() {
10413 		lineEdit.selectAll();
10414 	}
10415 
10416 	override void focus() {
10417 		lineEdit.focus();
10418 	}
10419 }
10420 
10421 /++
10422 	A labeled password edit.
10423 
10424 	History:
10425 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
10426 
10427 		The default parameters for the constructors were also removed on May 19, 2021
10428 +/
10429 alias LabeledPasswordEdit = Labeled!PasswordEdit;
10430 
10431 private string toMenuLabel(string s) {
10432 	string n;
10433 	n.reserve(s.length);
10434 	foreach(c; s)
10435 		if(c == '_')
10436 			n ~= ' ';
10437 		else
10438 			n ~= c;
10439 	return n;
10440 }
10441 
10442 private void autoExceptionHandler(Exception e) {
10443 	messageBox(e.msg);
10444 }
10445 
10446 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
10447 	static if(is(T : void delegate())) {
10448 		return () {
10449 			try
10450 				t();
10451 			catch(Exception e)
10452 				autoExceptionHandler(e);
10453 		};
10454 	} else static if(is(typeof(fn) Params == __parameters)) {
10455 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
10456 			return () {
10457 				void onOK(string s) {
10458 					member = s;
10459 					try
10460 						t(Params[0](s));
10461 					catch(Exception e)
10462 						autoExceptionHandler(e);
10463 				}
10464 
10465 				if(
10466 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
10467 					|| type == FileDialogType.Save)
10468 				{
10469 					getSaveFileName(window, &onOK, member, filters, null);
10470 				} else
10471 					getOpenFileName(window, &onOK, member, filters, null);
10472 			};
10473 		} else {
10474 			struct S {
10475 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
10476 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
10477 				} else mixin(q{
10478 				static foreach(idx, ignore; Params) {
10479 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
10480 				}
10481 				});
10482 			}
10483 			return () {
10484 				dialog(window, (S s) {
10485 					try {
10486 						static if(is(typeof(t) Ret == return)) {
10487 							static if(is(Ret == void)) {
10488 								t(s.tupleof);
10489 							} else {
10490 								auto ret = t(s.tupleof);
10491 								import std.conv;
10492 								messageBox(to!string(ret), "Returned Value");
10493 							}
10494 						}
10495 					} catch(Exception e)
10496 						autoExceptionHandler(e);
10497 				}, null, __traits(identifier, fn));
10498 			};
10499 		}
10500 	}
10501 }
10502 
10503 private template hasAnyRelevantAnnotations(a...) {
10504 	bool helper() {
10505 		bool any;
10506 		foreach(attr; a) {
10507 			static if(is(typeof(attr) == .menu))
10508 				any = true;
10509 			else static if(is(typeof(attr) == .toolbar))
10510 				any = true;
10511 			else static if(is(attr == .separator))
10512 				any = true;
10513 			else static if(is(typeof(attr) == .accelerator))
10514 				any = true;
10515 			else static if(is(typeof(attr) == .hotkey))
10516 				any = true;
10517 			else static if(is(typeof(attr) == .icon))
10518 				any = true;
10519 			else static if(is(typeof(attr) == .label))
10520 				any = true;
10521 			else static if(is(typeof(attr) == .tip))
10522 				any = true;
10523 		}
10524 		return any;
10525 	}
10526 
10527 	enum bool hasAnyRelevantAnnotations = helper();
10528 }
10529 
10530 /++
10531 	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.
10532 +/
10533 class MainWindow : Window {
10534 	///
10535 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
10536 		super(initialWidth, initialHeight, title);
10537 
10538 		_clientArea = new ClientAreaWidget();
10539 		_clientArea.x = 0;
10540 		_clientArea.y = 0;
10541 		_clientArea.width = this.width;
10542 		_clientArea.height = this.height;
10543 		_clientArea.tabStop = false;
10544 
10545 		super.addChild(_clientArea);
10546 
10547 		statusBar = new StatusBar(this);
10548 	}
10549 
10550 	/++
10551 		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).
10552 
10553 		The only required annotation on a function is `@menu("Label")` to make it appear, but there are several optional ones I'd recommend considering, including `@toolbar("group name")`, `@icon()`, `@accelerator("keyboard shortcut string")`, and `@hotkey('char')`.
10554 
10555 		You can also use `@separator` to put a separating line in the menu before the function.
10556 
10557 		Functions may have zero or one argument. If they have an argument, an automatic dialog box (see: [dialog]) will be created to request the data from the user before calling your function. Some types have special treatment, like [FileName], will invoke the file dialog, assuming open or save based on the name of your function.
10558 
10559 		Let's look at a complete example:
10560 
10561 	---
10562 	import arsd.minigui;
10563 
10564 	void main() {
10565 		auto window = new MainWindow();
10566 
10567 		// we can add widgets before or after setting the menu, either way is fine.
10568 		// i'll do it before here so the local variables are available to the commands.
10569 
10570 		auto textEdit = new TextEdit(window);
10571 
10572 		// Remember, in D, you can define structs inside of functions
10573 		// and those structs can access the function's local variables.
10574 		//
10575 		// Of course, you might also want to do this separately, and if you
10576 		// do, make sure you keep a reference to the window as a struct data
10577 		// member so you can refer to it in cases like this Exit function.
10578 		struct Commands {
10579 			// the & in the string indicates that the next letter is the hotkey
10580 			// to access it from the keyboard (so here, alt+f will open the
10581 			// file menu)
10582 			@menu("&File") {
10583 				@accelerator("Ctrl+N")
10584 				@hotkey('n')
10585 				@icon(GenericIcons.New) // add an icon to the action
10586 				@toolbar("File") // adds it to a toolbar.
10587 				// The toolbar name is never visible to the user, but is used to group icons.
10588 				void New() {
10589 					previousFileReferenced = null;
10590 					textEdit.content = "";
10591 				}
10592 
10593 				@icon(GenericIcons.Open)
10594 				@toolbar("File")
10595 				@hotkey('s')
10596 				@accelerator("Ctrl+O")
10597 				void Open(FileName!() filename) {
10598 					import std.file;
10599 					textEdit.content = std.file.readText(filename);
10600 				}
10601 
10602 				@icon(GenericIcons.Save)
10603 				@toolbar("File")
10604 				@accelerator("Ctrl+S")
10605 				@hotkey('s')
10606 				void Save() {
10607 					// these are still functions, so of course you can
10608 					// still call them yourself too
10609 					Save_As(previousFileReferenced);
10610 				}
10611 
10612 				// underscores translate to spaces in the visible name
10613 				@hotkey('a')
10614 				void Save_As(FileName!() filename) {
10615 					import std.file;
10616 					std.file.write(previousFileReferenced, textEdit.content);
10617 				}
10618 
10619 				// you can put the annotations before or after the function name+args and it works the same way
10620 				@separator
10621 				void Exit() @accelerator("Alt+F4") @hotkey('x') {
10622 					window.close();
10623 				}
10624 			}
10625 
10626 			@menu("&Edit") {
10627 				// not putting accelerators here because the text edit widget
10628 				// does it locally, so no need to duplicate it globally.
10629 
10630 				@icon(GenericIcons.Undo)
10631 				void Undo() @toolbar("Undo") {
10632 					textEdit.undo();
10633 				}
10634 
10635 				@separator
10636 
10637 				@icon(GenericIcons.Cut)
10638 				void Cut() @toolbar("Edit") {
10639 					textEdit.cut();
10640 				}
10641 				@icon(GenericIcons.Copy)
10642 				void Copy() @toolbar("Edit") {
10643 					textEdit.copy();
10644 				}
10645 				@icon(GenericIcons.Paste)
10646 				void Paste() @toolbar("Edit") {
10647 					textEdit.paste();
10648 				}
10649 
10650 				@separator
10651 				void Select_All() {
10652 					textEdit.selectAll();
10653 				}
10654 			}
10655 
10656 			@menu("Help") {
10657 				void About() @accelerator("F1") {
10658 					window.messageBox("A minigui sample program.");
10659 				}
10660 
10661 				// @label changes the name in the menu from what is in the code
10662 				@label("In Menu Name")
10663 				void otherNameInCode() {}
10664 			}
10665 		}
10666 
10667 		// declare the object that holds the commands, and set
10668 		// and members you want from it
10669 		Commands commands;
10670 
10671 		// and now tell minigui to do its magic and create the ui for it!
10672 		window.setMenuAndToolbarFromAnnotatedCode(commands);
10673 
10674 		// then, loop the window normally;
10675 		window.loop();
10676 
10677 		// important to note that the `commands` variable must live through the window's whole life cycle,
10678 		// or you can have crashes. If you declare the variable and loop in different functions, make sure
10679 		// you do `new Commands` so the garbage collector can take over management of it for you.
10680 	}
10681 	---
10682 
10683 	Note that you can call this function multiple times and it will add the items in order to the given items.
10684 
10685 	+/
10686 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
10687 		setMenuAndToolbarFromAnnotatedCode_internal(t);
10688 	}
10689 	/// ditto
10690 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
10691 		setMenuAndToolbarFromAnnotatedCode_internal(t);
10692 	}
10693 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
10694 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
10695 		Menu[string] mcs;
10696 
10697 		alias ToolbarSection = ToolBar.ToolbarSection;
10698 		ToolbarSection[] toolbarSections;
10699 
10700 		foreach(menu; menuBar.subMenus) {
10701 			mcs[menu.label] = menu;
10702 		}
10703 
10704 		foreach(memberName; __traits(derivedMembers, T)) {
10705 			static if(memberName != "this")
10706 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
10707 				.menu menu;
10708 				.toolbar toolbar;
10709 				bool separator;
10710 				.accelerator accelerator;
10711 				.hotkey hotkey;
10712 				.icon icon;
10713 				string label;
10714 				string tip;
10715 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
10716 					static if(is(typeof(attr) == .menu))
10717 						menu = attr;
10718 					else static if(is(typeof(attr) == .toolbar))
10719 						toolbar = attr;
10720 					else static if(is(attr == .separator))
10721 						separator = true;
10722 					else static if(is(typeof(attr) == .accelerator))
10723 						accelerator = attr;
10724 					else static if(is(typeof(attr) == .hotkey))
10725 						hotkey = attr;
10726 					else static if(is(typeof(attr) == .icon))
10727 						icon = attr;
10728 					else static if(is(typeof(attr) == .label))
10729 						label = attr.label;
10730 					else static if(is(typeof(attr) == .tip))
10731 						tip = attr.tip;
10732 				}
10733 
10734 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
10735 					ushort correctIcon = icon.id; // FIXME
10736 					if(label.length == 0)
10737 						label = memberName.toMenuLabel;
10738 
10739 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
10740 
10741 					auto action = new Action(label, correctIcon, handler);
10742 
10743 					if(accelerator.keyString.length) {
10744 						auto ke = KeyEvent.parse(accelerator.keyString);
10745 						action.accelerator = ke;
10746 						accelerators[ke.toStr] = handler;
10747 					}
10748 
10749 					if(toolbar !is .toolbar.init) {
10750 						bool found;
10751 						foreach(ref section; toolbarSections)
10752 							if(section.name == toolbar.groupName) {
10753 								section.actions ~= action;
10754 								found = true;
10755 								break;
10756 							}
10757 						if(!found) {
10758 							toolbarSections ~= ToolbarSection(toolbar.groupName, [action]);
10759 						}
10760 					}
10761 					if(menu !is .menu.init) {
10762 						Menu mc;
10763 						if(menu.name in mcs) {
10764 							mc = mcs[menu.name];
10765 						} else {
10766 							mc = new Menu(menu.name, this);
10767 							menuBar.addItem(mc);
10768 							mcs[menu.name] = mc;
10769 						}
10770 
10771 						if(separator)
10772 							mc.addSeparator();
10773 						auto mi = mc.addItem(new MenuItem(action));
10774 
10775 						if(hotkey !is .hotkey.init)
10776 							mi.hotkey = hotkey.ch;
10777 					}
10778 				}
10779 			}
10780 		}
10781 
10782 		this.menuBar = menuBar;
10783 
10784 		if(toolbarSections.length) {
10785 			auto tb = new ToolBar(toolbarSections, this);
10786 		}
10787 	}
10788 
10789 	void delegate()[string] accelerators;
10790 
10791 	override void defaultEventHandler_keydown(KeyDownEvent event) {
10792 		auto str = event.originalKeyEvent.toStr;
10793 		if(auto acl = str in accelerators)
10794 			(*acl)();
10795 
10796 		// Windows this this automatically so only on custom need we implement it
10797 		version(custom_widgets) {
10798 			if(event.altKey && this.menuBar) {
10799 				foreach(item; this.menuBar.items) {
10800 					if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) {
10801 						// FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code
10802 						item.dynamicState = DynamicState.hover | DynamicState.depressed;
10803 						item.redraw();
10804 						auto e = new MouseDownEvent(item);
10805 						e.dispatch();
10806 						break;
10807 					}
10808 				}
10809 			}
10810 
10811 			if(event.key == Key.Menu) {
10812 				showContextMenu(-1, -1);
10813 			}
10814 		}
10815 
10816 		super.defaultEventHandler_keydown(event);
10817 	}
10818 
10819 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
10820 		super.defaultEventHandler_mouseover(event);
10821 		if(this.statusBar !is null && event.target.statusTip.length)
10822 			this.statusBar.parts[0].content = event.target.statusTip;
10823 		else if(this.statusBar !is null && this.statusTip.length)
10824 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
10825 	}
10826 
10827 	override void addChild(Widget c, int position = int.max) {
10828 		if(auto tb = cast(ToolBar) c)
10829 			version(win32_widgets)
10830 				super.addChild(c, 0);
10831 			else version(custom_widgets)
10832 				super.addChild(c, menuBar ? 1 : 0);
10833 			else static assert(0);
10834 		else
10835 			clientArea.addChild(c, position);
10836 	}
10837 
10838 	ToolBar _toolBar;
10839 	///
10840 	ToolBar toolBar() { return _toolBar; }
10841 	///
10842 	ToolBar toolBar(ToolBar t) {
10843 		_toolBar = t;
10844 		foreach(child; this.children)
10845 			if(child is t)
10846 				return t;
10847 		version(win32_widgets)
10848 			super.addChild(t, 0);
10849 		else version(custom_widgets)
10850 			super.addChild(t, menuBar ? 1 : 0);
10851 		else static assert(0);
10852 		return t;
10853 	}
10854 
10855 	MenuBar _menu;
10856 	///
10857 	MenuBar menuBar() { return _menu; }
10858 	///
10859 	MenuBar menuBar(MenuBar m) {
10860 		if(m is _menu) {
10861 			version(custom_widgets)
10862 				queueRecomputeChildLayout();
10863 			return m;
10864 		}
10865 
10866 		if(_menu !is null) {
10867 			// make sure it is sanely removed
10868 			// FIXME
10869 		}
10870 
10871 		_menu = m;
10872 
10873 		version(win32_widgets) {
10874 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
10875 		} else version(custom_widgets) {
10876 			super.addChild(m, 0);
10877 
10878 		//	clientArea.y = menu.height;
10879 		//	clientArea.height = this.height - menu.height;
10880 
10881 			queueRecomputeChildLayout();
10882 		} else static assert(false);
10883 
10884 		return _menu;
10885 	}
10886 	private Widget _clientArea;
10887 	///
10888 	@property Widget clientArea() { return _clientArea; }
10889 	protected @property void clientArea(Widget wid) {
10890 		_clientArea = wid;
10891 	}
10892 
10893 	private StatusBar _statusBar;
10894 	/++
10895 		Returns the window's [StatusBar]. Be warned it may be `null`.
10896 	+/
10897 	@property StatusBar statusBar() { return _statusBar; }
10898 	/// ditto
10899 	@property void statusBar(StatusBar bar) {
10900 		if(_statusBar !is null)
10901 			_statusBar.removeWidget();
10902 		_statusBar = bar;
10903 		if(bar !is null)
10904 			super.addChild(_statusBar);
10905 	}
10906 }
10907 
10908 /+
10909 	This is really an implementation detail of [MainWindow]
10910 +/
10911 private class ClientAreaWidget : Widget {
10912 	this() {
10913 		this.tabStop = false;
10914 		super(null);
10915 		//sa = new ScrollableWidget(this);
10916 	}
10917 	/*
10918 	ScrollableWidget sa;
10919 	override void addChild(Widget w, int position) {
10920 		if(sa is null)
10921 			super.addChild(w, position);
10922 		else {
10923 			sa.addChild(w, position);
10924 			sa.setContentSize(this.minWidth + 1, this.minHeight);
10925 			writeln(sa.contentWidth, "x", sa.contentHeight);
10926 		}
10927 	}
10928 	*/
10929 }
10930 
10931 /**
10932 	Toolbars are lists of buttons (typically icons) that appear under the menu.
10933 	Each button ought to correspond to a menu item, represented by [Action] objects.
10934 */
10935 class ToolBar : Widget {
10936 	version(win32_widgets) {
10937 		private int idealHeight;
10938 		override int minHeight() { return idealHeight; }
10939 		override int maxHeight() { return idealHeight; }
10940 	} else version(custom_widgets) {
10941 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
10942 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
10943 	} else static assert(false);
10944 	override int heightStretchiness() { return 0; }
10945 
10946 	static struct ToolbarSection {
10947 		string name;
10948 		Action[] actions;
10949 	}
10950 
10951 	version(win32_widgets) {
10952 		HIMAGELIST imageListSmall;
10953 		HIMAGELIST imageListLarge;
10954 	}
10955 
10956 	this(Widget parent) {
10957 		this(cast(ToolbarSection[]) null, parent);
10958 	}
10959 
10960 	version(win32_widgets)
10961 	void changeIconSize(bool useLarge) {
10962 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
10963 
10964 		/+
10965 		SIZE size;
10966 		import core.sys.windows.commctrl;
10967 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
10968 		idealHeight = size.cy + 4; // the plus 4 is a hack
10969 		+/
10970 
10971 		idealHeight = useLarge ? 34 : 26;
10972 
10973 		if(parent) {
10974 			parent.queueRecomputeChildLayout();
10975 			parent.redraw();
10976 		}
10977 
10978 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
10979 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
10980 	}
10981 
10982 	/++
10983 		History:
10984 			The `ToolbarSection` overload was added December 31, 2024
10985 	+/
10986 	this(Action[] actions, Widget parent) {
10987 		this([ToolbarSection(null, actions)], parent);
10988 	}
10989 
10990 	/// ditto
10991 	this(ToolbarSection[] sections, Widget parent) {
10992 		super(parent);
10993 
10994 		tabStop = false;
10995 
10996 		version(win32_widgets) {
10997 			// so i like how the flat thing looks on windows, but not on wine
10998 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
10999 			// leave it commented
11000 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
11001 
11002 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
11003 
11004 			imageListSmall = ImageList_Create(
11005 				// width, height
11006 				16, 16,
11007 				ILC_COLOR16 | ILC_MASK,
11008 				16 /*numberOfButtons*/, 0);
11009 
11010 			imageListLarge = ImageList_Create(
11011 				// width, height
11012 				24, 24,
11013 				ILC_COLOR16 | ILC_MASK,
11014 				16 /*numberOfButtons*/, 0);
11015 
11016 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
11017 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
11018 
11019 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
11020 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
11021 
11022 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
11023 
11024 			TBBUTTON[] buttons;
11025 
11026 			// FIXME: I_IMAGENONE is if here is no icon
11027 			foreach(sidx, section; sections) {
11028 				if(sidx)
11029 					buttons ~= TBBUTTON(
11030 						scaleWithDpi(4),
11031 						0,
11032 						TBSTATE_ENABLED, // state
11033 						TBSTYLE_SEP | BTNS_SEP, // style
11034 						0, // reserved array, just zero it out
11035 						0, // dwData
11036 						-1
11037 					);
11038 
11039 				foreach(action; section.actions)
11040 					buttons ~= TBBUTTON(
11041 						MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
11042 						action.id,
11043 						TBSTATE_ENABLED, // state
11044 						0, // style
11045 						0, // reserved array, just zero it out
11046 						0, // dwData
11047 						cast(size_t) toWstringzInternal(action.label) // INT_PTR
11048 					);
11049 			}
11050 
11051 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
11052 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
11053 
11054 			/*
11055 			RECT rect;
11056 			GetWindowRect(hwnd, &rect);
11057 			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
11058 			*/
11059 
11060 			dpiChanged(); // to load the things calling changeIconSize the first time
11061 
11062 			assert(idealHeight);
11063 		} else version(custom_widgets) {
11064 			foreach(sidx, section; sections) {
11065 				if(sidx)
11066 					new HorizontalSpacer(4, this);
11067 				foreach(action; section.actions)
11068 					new ToolButton(action, this);
11069 			}
11070 		} else static assert(false);
11071 	}
11072 
11073 	override void recomputeChildLayout() {
11074 		.recomputeChildLayout!"width"(this);
11075 	}
11076 
11077 
11078 	version(win32_widgets)
11079 	override protected void dpiChanged() {
11080 		auto sz = scaleWithDpi(16);
11081 		if(sz >= 20)
11082 			changeIconSize(true);
11083 		else
11084 			changeIconSize(false);
11085 	}
11086 }
11087 
11088 enum toolbarIconSize = 24;
11089 
11090 /// 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.
11091 class ToolButton : Button {
11092 	///
11093 	this(Action action, Widget parent) {
11094 		super(action.label, parent);
11095 		tabStop = false;
11096 		this.action = action;
11097 	}
11098 
11099 	version(custom_widgets)
11100 	override void defaultEventHandler_click(ClickEvent event) {
11101 		foreach(handler; action.triggered)
11102 			handler();
11103 	}
11104 
11105 	Action action;
11106 
11107 	override int maxWidth() { return toolbarIconSize; }
11108 	override int minWidth() { return toolbarIconSize; }
11109 	override int maxHeight() { return toolbarIconSize; }
11110 	override int minHeight() { return toolbarIconSize; }
11111 
11112 	version(custom_widgets)
11113 	override void paint(WidgetPainter painter) {
11114 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
11115 		painter.outlineColor = Color.black;
11116 
11117 		// I want to get from 16 to 24. that's * 3 / 2
11118 		static assert(toolbarIconSize >= 16);
11119 		enum multiplier = toolbarIconSize / 8;
11120 		enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0);
11121 		switch(action.iconId) {
11122 			case GenericIcons.New:
11123 				painter.fillColor = Color.white;
11124 				painter.drawPolygon(
11125 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor,
11126 					Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor,
11127 					Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor
11128 				);
11129 			break;
11130 			case GenericIcons.Save:
11131 				painter.fillColor = Color.white;
11132 				painter.outlineColor = Color.black;
11133 				painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
11134 
11135 				// the label
11136 				painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor);
11137 
11138 				// the slider
11139 				painter.fillColor = Color.black;
11140 				painter.outlineColor = Color.black;
11141 				painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor);
11142 
11143 				painter.fillColor = Color.white;
11144 				painter.outlineColor = Color.white;
11145 				// the disc window
11146 				painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor);
11147 			break;
11148 			case GenericIcons.Open:
11149 				painter.fillColor = Color.white;
11150 				painter.drawPolygon(
11151 					Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor,
11152 					Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor);
11153 				painter.drawPolygon(
11154 					Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor,
11155 					Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor,
11156 					Point(2, 6) * multiplier / divisor);
11157 				//painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor);
11158 			break;
11159 			case GenericIcons.Copy:
11160 				painter.fillColor = Color.white;
11161 				painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor);
11162 				painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor);
11163 			break;
11164 			case GenericIcons.Cut:
11165 				painter.fillColor = Color.transparent;
11166 				painter.outlineColor = getComputedStyle.foregroundColor();
11167 				painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor);
11168 				painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor);
11169 				painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor);
11170 				painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor);
11171 			break;
11172 			case GenericIcons.Paste:
11173 				painter.fillColor = Color.white;
11174 				painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor);
11175 				painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor);
11176 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor);
11177 				painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor);
11178 				painter.fillColor = Color.black;
11179 				painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor);
11180 			break;
11181 			case GenericIcons.Help:
11182 				painter.outlineColor = getComputedStyle.foregroundColor();
11183 				painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11184 			break;
11185 			case GenericIcons.Undo:
11186 				painter.fillColor = Color.transparent;
11187 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
11188 				painter.outlineColor = Color.black;
11189 				painter.fillColor = Color.black;
11190 				painter.drawPolygon(
11191 					Point(4, 4) * multiplier / divisor,
11192 					Point(8, 2) * multiplier / divisor,
11193 					Point(8, 6) * multiplier / divisor,
11194 					Point(4, 4) * multiplier / divisor,
11195 				);
11196 			break;
11197 			case GenericIcons.Redo:
11198 				painter.fillColor = Color.transparent;
11199 				painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64);
11200 				painter.outlineColor = Color.black;
11201 				painter.fillColor = Color.black;
11202 				painter.drawPolygon(
11203 					Point(10, 4) * multiplier / divisor,
11204 					Point(6, 2) * multiplier / divisor,
11205 					Point(6, 6) * multiplier / divisor,
11206 					Point(10, 4) * multiplier / divisor,
11207 				);
11208 			break;
11209 			default:
11210 				painter.outlineColor = getComputedStyle.foregroundColor;
11211 				painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11212 		}
11213 		return bounds;
11214 		});
11215 	}
11216 
11217 }
11218 
11219 
11220 /++
11221 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
11222 +/
11223 class MenuBar : Widget {
11224 	MenuItem[] items;
11225 	Menu[] subMenus;
11226 
11227 	version(win32_widgets) {
11228 		HMENU handle;
11229 		///
11230 		this(Widget parent = null) {
11231 			super(parent);
11232 
11233 			handle = CreateMenu();
11234 			tabStop = false;
11235 		}
11236 	} else version(custom_widgets) {
11237 		///
11238 		this(Widget parent = null) {
11239 			tabStop = false; // these are selected some other way
11240 			super(parent);
11241 		}
11242 
11243 		mixin Padding!q{2};
11244 	} else static assert(false);
11245 
11246 	version(custom_widgets)
11247 	override void paint(WidgetPainter painter) {
11248 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
11249 	}
11250 
11251 	///
11252 	MenuItem addItem(MenuItem item) {
11253 		this.addChild(item);
11254 		items ~= item;
11255 		version(win32_widgets) {
11256 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11257 		}
11258 		return item;
11259 	}
11260 
11261 
11262 	///
11263 	Menu addItem(Menu item) {
11264 
11265 		subMenus ~= item;
11266 
11267 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
11268 
11269 		addChild(mbItem);
11270 		items ~= mbItem;
11271 
11272 		version(win32_widgets) {
11273 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
11274 		} else version(custom_widgets) {
11275 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
11276 				item.popup(mbItem);
11277 			};
11278 		} else static assert(false);
11279 
11280 		return item;
11281 	}
11282 
11283 	override void recomputeChildLayout() {
11284 		.recomputeChildLayout!"width"(this);
11285 	}
11286 
11287 	override int maxHeight() { return defaultLineHeight + 4; }
11288 	override int minHeight() { return defaultLineHeight + 4; }
11289 }
11290 
11291 
11292 /**
11293 	Status bars appear at the bottom of a MainWindow.
11294 	They are made out of Parts, with a width and content.
11295 
11296 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
11297 
11298 
11299 	sb.parts[0].content = "Status bar text!";
11300 */
11301 class StatusBar : Widget {
11302 	private Part[] partsArray;
11303 	///
11304 	struct Parts {
11305 		@disable this();
11306 		this(StatusBar owner) { this.owner = owner; }
11307 		//@disable this(this);
11308 		///
11309 		@property int length() { return cast(int) owner.partsArray.length; }
11310 		private StatusBar owner;
11311 		private this(StatusBar owner, Part[] parts) {
11312 			this.owner.partsArray = parts;
11313 			this.owner = owner;
11314 		}
11315 		///
11316 		Part opIndex(int p) {
11317 			if(owner.partsArray.length == 0)
11318 				this ~= new StatusBar.Part(0);
11319 			return owner.partsArray[p];
11320 		}
11321 
11322 		///
11323 		Part opOpAssign(string op : "~" )(Part p) {
11324 			assert(owner.partsArray.length < 255);
11325 			p.owner = this.owner;
11326 			p.idx = cast(int) owner.partsArray.length;
11327 			owner.partsArray ~= p;
11328 
11329 			owner.queueRecomputeChildLayout();
11330 
11331 			version(win32_widgets) {
11332 				int[256] pos;
11333 				int cpos;
11334 				foreach(idx, part; owner.partsArray) {
11335 					if(idx + 1 == owner.partsArray.length)
11336 						pos[idx] = -1;
11337 					else {
11338 						cpos += part.currentlyAssignedWidth;
11339 						pos[idx] = cpos;
11340 					}
11341 				}
11342 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
11343 			} else version(custom_widgets) {
11344 				owner.redraw();
11345 			} else static assert(false);
11346 
11347 			return p;
11348 		}
11349 
11350 		/++
11351 			Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions.
11352 
11353 			No given item should be 0.
11354 
11355 			History:
11356 				Added December 31, 2024
11357 		+/
11358 		void setSizes(int[] proportions...) {
11359 			assert(this.owner);
11360 			this.owner.partsArray = null;
11361 
11362 			foreach(n; proportions) {
11363 				assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead.");
11364 
11365 				this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels));
11366 			}
11367 
11368 		}
11369 	}
11370 
11371 	private Parts _parts;
11372 	///
11373 	final @property Parts parts() {
11374 		return _parts;
11375 	}
11376 
11377 	/++
11378 
11379 	+/
11380 	static class Part {
11381 		/++
11382 			History:
11383 				Added September 1, 2023 (dub v11.1)
11384 		+/
11385 		enum WidthUnits {
11386 			/++
11387 				Unscaled pixels as they appear on screen.
11388 
11389 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
11390 			+/
11391 			DeviceDependentPixels,
11392 			/++
11393 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
11394 			+/
11395 			DeviceIndependentPixels,
11396 			/++
11397 				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`).
11398 			+/
11399 			ApproximateCharacters,
11400 			/++
11401 				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.
11402 
11403 				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.
11404 			+/
11405 			Proportional
11406 		}
11407 		private WidthUnits units;
11408 		private int width;
11409 		private StatusBar owner;
11410 
11411 		private int currentlyAssignedWidth;
11412 
11413 		/++
11414 			History:
11415 				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.
11416 
11417 				It now allows you to provide your own value for [WidthUnits].
11418 
11419 				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`.
11420 		+/
11421 		this(int w, WidthUnits units = WidthUnits.Proportional) {
11422 			this.units = units;
11423 			this.width = w;
11424 		}
11425 
11426 		/// ditto
11427 		this(int w = 0) {
11428 			if(w == 0)
11429 				this(w, WidthUnits.Proportional);
11430 			else
11431 				this(w, WidthUnits.DeviceDependentPixels);
11432 		}
11433 
11434 		private int idx;
11435 		private string _content;
11436 		///
11437 		@property string content() { return _content; }
11438 		///
11439 		@property void content(string s) {
11440 			version(win32_widgets) {
11441 				_content = s;
11442 				WCharzBuffer bfr = WCharzBuffer(s);
11443 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
11444 			} else version(custom_widgets) {
11445 				if(_content != s) {
11446 					_content = s;
11447 					owner.redraw();
11448 				}
11449 			} else static assert(false);
11450 		}
11451 	}
11452 	string simpleModeContent;
11453 	bool inSimpleMode;
11454 
11455 
11456 	///
11457 	this(Widget parent) {
11458 		super(null); // FIXME
11459 		_parts = Parts(this);
11460 		tabStop = false;
11461 		version(win32_widgets) {
11462 			parentWindow = parent.parentWindow;
11463 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
11464 
11465 			RECT rect;
11466 			GetWindowRect(hwnd, &rect);
11467 			idealHeight = rect.bottom - rect.top;
11468 			assert(idealHeight);
11469 		} else version(custom_widgets) {
11470 		} else static assert(false);
11471 	}
11472 
11473 	override void recomputeChildLayout() {
11474 		int remainingLength = this.width;
11475 
11476 		int proportionalSum;
11477 		int proportionalCount;
11478 		foreach(idx, part; this.partsArray) {
11479 			with(Part.WidthUnits)
11480 			final switch(part.units) {
11481 				case DeviceDependentPixels:
11482 					part.currentlyAssignedWidth = part.width;
11483 					remainingLength -= part.currentlyAssignedWidth;
11484 				break;
11485 				case DeviceIndependentPixels:
11486 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
11487 					remainingLength -= part.currentlyAssignedWidth;
11488 				break;
11489 				case ApproximateCharacters:
11490 					auto cs = getComputedStyle();
11491 					auto font = cs.font;
11492 
11493 					part.currentlyAssignedWidth = font.averageWidth * this.width;
11494 					remainingLength -= part.currentlyAssignedWidth;
11495 				break;
11496 				case Proportional:
11497 					proportionalSum += part.width;
11498 					proportionalCount ++;
11499 				break;
11500 			}
11501 		}
11502 
11503 		foreach(part; this.partsArray) {
11504 			if(part.units == Part.WidthUnits.Proportional) {
11505 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
11506 				if(proportion == 0)
11507 					proportion = 1;
11508 
11509 				if(proportionalSum == 0)
11510 					proportionalSum = proportionalCount;
11511 
11512 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
11513 			}
11514 		}
11515 
11516 		super.recomputeChildLayout();
11517 	}
11518 
11519 	version(win32_widgets)
11520 	override protected void dpiChanged() {
11521 		RECT rect;
11522 		GetWindowRect(hwnd, &rect);
11523 		idealHeight = rect.bottom - rect.top;
11524 		assert(idealHeight);
11525 	}
11526 
11527 	version(custom_widgets)
11528 	override void paint(WidgetPainter painter) {
11529 		auto cs = getComputedStyle();
11530 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11531 		int cpos = 0;
11532 		foreach(idx, part; this.partsArray) {
11533 			auto partWidth = part.currentlyAssignedWidth;
11534 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
11535 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
11536 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
11537 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
11538 
11539 			painter.outlineColor = cs.foregroundColor();
11540 			painter.fillColor = cs.foregroundColor();
11541 
11542 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
11543 			cpos += partWidth;
11544 		}
11545 	}
11546 
11547 
11548 	version(win32_widgets) {
11549 		private int idealHeight;
11550 		override int maxHeight() { return idealHeight; }
11551 		override int minHeight() { return idealHeight; }
11552 	} else version(custom_widgets) {
11553 		override int maxHeight() { return defaultLineHeight + 4; }
11554 		override int minHeight() { return defaultLineHeight + 4; }
11555 	} else static assert(false);
11556 }
11557 
11558 /// Displays an in-progress indicator without known values
11559 version(none)
11560 class IndefiniteProgressBar : Widget {
11561 	version(win32_widgets)
11562 	this(Widget parent) {
11563 		super(parent);
11564 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
11565 		tabStop = false;
11566 	}
11567 	override int minHeight() { return 10; }
11568 }
11569 
11570 /// A progress bar with a known endpoint and completion amount
11571 class ProgressBar : Widget {
11572 	/++
11573 		History:
11574 			Added March 16, 2022 (dub v10.7)
11575 	+/
11576 	this(int min, int max, Widget parent) {
11577 		this(parent);
11578 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
11579 	}
11580 	this(Widget parent) {
11581 		version(win32_widgets) {
11582 			super(parent);
11583 			createWin32Window(this, "msctls_progress32"w, "", 0);
11584 			tabStop = false;
11585 		} else version(custom_widgets) {
11586 			super(parent);
11587 			max = 100;
11588 			step = 10;
11589 			tabStop = false;
11590 		} else static assert(0);
11591 	}
11592 
11593 	version(custom_widgets)
11594 	override void paint(WidgetPainter painter) {
11595 		auto cs = getComputedStyle();
11596 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11597 		painter.fillColor = cs.progressBarColor;
11598 		painter.drawRectangle(Point(0, 0), width * current / max, height);
11599 	}
11600 
11601 
11602 	version(custom_widgets) {
11603 		int current;
11604 		int max;
11605 		int step;
11606 	}
11607 
11608 	///
11609 	void advanceOneStep() {
11610 		version(win32_widgets)
11611 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
11612 		else version(custom_widgets)
11613 			addToPosition(step);
11614 		else static assert(false);
11615 	}
11616 
11617 	///
11618 	void setStepIncrement(int increment) {
11619 		version(win32_widgets)
11620 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
11621 		else version(custom_widgets)
11622 			step = increment;
11623 		else static assert(false);
11624 	}
11625 
11626 	///
11627 	void addToPosition(int amount) {
11628 		version(win32_widgets)
11629 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
11630 		else version(custom_widgets)
11631 			setPosition(current + amount);
11632 		else static assert(false);
11633 	}
11634 
11635 	///
11636 	void setPosition(int pos) {
11637 		version(win32_widgets)
11638 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
11639 		else version(custom_widgets) {
11640 			current = pos;
11641 			if(current > max)
11642 				current = max;
11643 			redraw();
11644 		}
11645 		else static assert(false);
11646 	}
11647 
11648 	///
11649 	void setRange(ushort min, ushort max) {
11650 		version(win32_widgets)
11651 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
11652 		else version(custom_widgets) {
11653 			this.max = max;
11654 		}
11655 		else static assert(false);
11656 	}
11657 
11658 	override int minHeight() { return 10; }
11659 }
11660 
11661 version(custom_widgets)
11662 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
11663 	thisLabel.reserve(label.length);
11664 	bool justSawAmpersand;
11665 	foreach(ch; label) {
11666 		if(justSawAmpersand) {
11667 			justSawAmpersand = false;
11668 			if(ch == '&') {
11669 				goto plain;
11670 			}
11671 			thisAccelerator = ch;
11672 		} else {
11673 			if(ch == '&') {
11674 				justSawAmpersand = true;
11675 				continue;
11676 			}
11677 			plain:
11678 			thisLabel ~= ch;
11679 		}
11680 	}
11681 }
11682 
11683 /++
11684 	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.
11685 
11686 
11687 	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
11688 
11689 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11690 
11691 	History:
11692 		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.
11693 +/
11694 class Fieldset : Widget {
11695 	// FIXME: on Windows,it doesn't draw the background on the label
11696 	// on X, it doesn't fix the clipping rectangle for it
11697 	version(win32_widgets)
11698 		override int paddingTop() { return defaultLineHeight; }
11699 	else version(custom_widgets)
11700 		override int paddingTop() { return defaultLineHeight + 2; }
11701 	else static assert(false);
11702 	override int paddingBottom() { return 6; }
11703 	override int paddingLeft() { return 6; }
11704 	override int paddingRight() { return 6; }
11705 
11706 	override int marginLeft() { return 6; }
11707 	override int marginRight() { return 6; }
11708 	override int marginTop() { return 2; }
11709 	override int marginBottom() { return 2; }
11710 
11711 	string legend;
11712 
11713 	version(custom_widgets) private dchar accelerator;
11714 
11715 	this(string legend, Widget parent) {
11716 		version(win32_widgets) {
11717 			super(parent);
11718 			this.legend = legend;
11719 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
11720 			tabStop = false;
11721 		} else version(custom_widgets) {
11722 			super(parent);
11723 			tabStop = false;
11724 
11725 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
11726 		} else static assert(0);
11727 	}
11728 
11729 	version(custom_widgets)
11730 	override void paint(WidgetPainter painter) {
11731 		auto dlh = defaultLineHeight;
11732 
11733 		painter.fillColor = Color.transparent;
11734 		auto cs = getComputedStyle();
11735 		painter.pen = Pen(cs.foregroundColor, 1);
11736 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
11737 
11738 		auto tx = painter.textSize(legend);
11739 		painter.outlineColor = Color.transparent;
11740 
11741 		version(Windows) {
11742 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
11743 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
11744 			SelectObject(painter.impl.hdc, b);
11745 		} else static if(UsingSimpledisplayX11) {
11746 			painter.fillColor = getComputedStyle().windowBackgroundColor;
11747 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
11748 		}
11749 		painter.outlineColor = cs.foregroundColor;
11750 		painter.drawText(Point(8, 0), legend);
11751 	}
11752 
11753 	override int maxHeight() {
11754 		auto m = paddingTop() + paddingBottom();
11755 		foreach(child; children) {
11756 			auto mh = child.maxHeight();
11757 			if(mh == int.max)
11758 				return int.max;
11759 			m += mh;
11760 			m += child.marginBottom();
11761 			m += child.marginTop();
11762 		}
11763 		m += 6;
11764 		if(m < minHeight)
11765 			return minHeight;
11766 		return m;
11767 	}
11768 
11769 	override int minHeight() {
11770 		auto m = paddingTop() + paddingBottom();
11771 		foreach(child; children) {
11772 			m += child.minHeight();
11773 			m += child.marginBottom();
11774 			m += child.marginTop();
11775 		}
11776 		return m + 6;
11777 	}
11778 
11779 	override int minWidth() {
11780 		return 6 + cast(int) this.legend.length * 7;
11781 	}
11782 }
11783 
11784 /++
11785 	$(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")
11786 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
11787 +/
11788 version(minigui_screenshots)
11789 @Screenshot("Fieldset")
11790 unittest {
11791 	auto window = new Window(200, 100);
11792 	auto set = new Fieldset("Baby will", window);
11793 	auto option1 = new Radiobox("Eat", set);
11794 	auto option2 = new Radiobox("Cry", set);
11795 	auto option3 = new Radiobox("Sleep", set);
11796 	window.loop();
11797 }
11798 
11799 /// Draws a line
11800 class HorizontalRule : Widget {
11801 	mixin Margin!q{ 2 };
11802 	override int minHeight() { return 2; }
11803 	override int maxHeight() { return 2; }
11804 
11805 	///
11806 	this(Widget parent) {
11807 		super(parent);
11808 	}
11809 
11810 	override void paint(WidgetPainter painter) {
11811 		auto cs = getComputedStyle();
11812 		painter.outlineColor = cs.darkAccentColor;
11813 		painter.drawLine(Point(0, 0), Point(width, 0));
11814 		painter.outlineColor = cs.lightAccentColor;
11815 		painter.drawLine(Point(0, 1), Point(width, 1));
11816 	}
11817 }
11818 
11819 version(minigui_screenshots)
11820 @Screenshot("HorizontalRule")
11821 /++
11822 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
11823 
11824 +/
11825 unittest {
11826 	auto window = new Window(200, 100);
11827 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
11828 	new HorizontalRule(window);
11829 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
11830 	window.loop();
11831 }
11832 
11833 /// ditto
11834 class VerticalRule : Widget {
11835 	mixin Margin!q{ 2 };
11836 	override int minWidth() { return 2; }
11837 	override int maxWidth() { return 2; }
11838 
11839 	///
11840 	this(Widget parent) {
11841 		super(parent);
11842 	}
11843 
11844 	override void paint(WidgetPainter painter) {
11845 		auto cs = getComputedStyle();
11846 		painter.outlineColor = cs.darkAccentColor;
11847 		painter.drawLine(Point(0, 0), Point(0, height));
11848 		painter.outlineColor = cs.lightAccentColor;
11849 		painter.drawLine(Point(1, 0), Point(1, height));
11850 	}
11851 }
11852 
11853 
11854 ///
11855 class Menu : Window {
11856 	void remove() {
11857 		foreach(i, child; parentWindow.children)
11858 			if(child is this) {
11859 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
11860 				break;
11861 			}
11862 		parentWindow.redraw();
11863 
11864 		parentWindow.releaseMouseCapture();
11865 	}
11866 
11867 	///
11868 	void addSeparator() {
11869 		version(win32_widgets)
11870 			AppendMenu(handle, MF_SEPARATOR, 0, null);
11871 		else version(custom_widgets)
11872 			auto hr = new HorizontalRule(this);
11873 		else static assert(0);
11874 	}
11875 
11876 	override int paddingTop() { return 4; }
11877 	override int paddingBottom() { return 4; }
11878 	override int paddingLeft() { return 2; }
11879 	override int paddingRight() { return 2; }
11880 
11881 	version(win32_widgets) {}
11882 	else version(custom_widgets) {
11883 
11884 		Widget previouslyFocusedWidget;
11885 		Widget* previouslyFocusedWidgetBelongsIn;
11886 
11887 		SimpleWindow dropDown;
11888 		Widget menuParent;
11889 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
11890 			this.menuParent = parent;
11891 
11892 			previouslyFocusedWidget = parent.parentWindow.focusedWidget;
11893 			previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget;
11894 			parent.parentWindow.focusedWidget = this;
11895 
11896 			int w = 150;
11897 			int h = paddingTop + paddingBottom;
11898 			if(this.children.length) {
11899 				// hacking it to get the ideal height out of recomputeChildLayout
11900 				this.width = w;
11901 				this.height = h;
11902 				this.recomputeChildLayoutEntry();
11903 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
11904 				h += paddingBottom;
11905 
11906 				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
11907 			}
11908 
11909 			if(offsetY == int.min)
11910 				offsetY = parent.defaultLineHeight;
11911 
11912 			auto coord = parent.globalCoordinates();
11913 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
11914 			this.x = 0;
11915 			this.y = 0;
11916 			this.width = dropDown.width;
11917 			this.height = dropDown.height;
11918 			this.drawableWindow = dropDown;
11919 			this.recomputeChildLayoutEntry();
11920 
11921 			static if(UsingSimpledisplayX11)
11922 				XSync(XDisplayConnection.get, 0);
11923 
11924 			dropDown.visibilityChanged = (bool visible) {
11925 				if(visible) {
11926 					this.redraw();
11927 					dropDown.grabInput();
11928 				} else {
11929 					dropDown.releaseInputGrab();
11930 				}
11931 			};
11932 
11933 			dropDown.show();
11934 
11935 			clickListener = this.addEventListener((scope ClickEvent ev) {
11936 				unpopup();
11937 				// need to unlock asap just in case other user handlers block...
11938 				static if(UsingSimpledisplayX11)
11939 					flushGui();
11940 			}, true /* again for asap action */);
11941 		}
11942 
11943 		EventListener clickListener;
11944 	}
11945 	else static assert(false);
11946 
11947 	version(custom_widgets)
11948 	void unpopup() {
11949 		mouseLastOver = mouseLastDownOn = null;
11950 		dropDown.hide();
11951 		if(!menuParent.parentWindow.win.closed) {
11952 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
11953 				maw.setDynamicState(DynamicState.depressed, false);
11954 				maw.setDynamicState(DynamicState.hover, false);
11955 				maw.redraw();
11956 			}
11957 			// menuParent.parentWindow.win.focus();
11958 		}
11959 		clickListener.disconnect();
11960 
11961 		if(previouslyFocusedWidgetBelongsIn)
11962 			*previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget;
11963 	}
11964 
11965 	MenuItem[] items;
11966 
11967 	///
11968 	MenuItem addItem(MenuItem item) {
11969 		addChild(item);
11970 		items ~= item;
11971 		version(win32_widgets) {
11972 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11973 		}
11974 		return item;
11975 	}
11976 
11977 	string label;
11978 
11979 	version(win32_widgets) {
11980 		HMENU handle;
11981 		///
11982 		this(string label, Widget parent) {
11983 			// not actually passing the parent since it effs up the drawing
11984 			super(cast(Widget) null);// parent);
11985 			this.label = label;
11986 			handle = CreatePopupMenu();
11987 		}
11988 	} else version(custom_widgets) {
11989 		///
11990 		this(string label, Widget parent) {
11991 
11992 			if(dropDown) {
11993 				dropDown.close();
11994 			}
11995 			dropDown = new SimpleWindow(
11996 				150, 4,
11997 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
11998 
11999 			this.label = label;
12000 
12001 			super(dropDown);
12002 		}
12003 	} else static assert(false);
12004 
12005 	override int maxHeight() { return defaultLineHeight; }
12006 	override int minHeight() { return defaultLineHeight; }
12007 
12008 	version(custom_widgets) {
12009 		Widget currentPlace;
12010 
12011 		void changeCurrentPlace(Widget n) {
12012 			if(currentPlace) {
12013 				currentPlace.dynamicState = 0;
12014 			}
12015 
12016 			if(n) {
12017 				n.dynamicState = DynamicState.hover;
12018 			}
12019 
12020 			currentPlace = n;
12021 		}
12022 
12023 		override void paint(WidgetPainter painter) {
12024 			this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
12025 		}
12026 
12027 		override void defaultEventHandler_keydown(KeyDownEvent ke) {
12028 			switch(ke.key) {
12029 				case Key.Down:
12030 					Widget next;
12031 					Widget first;
12032 					foreach(w; this.children) {
12033 						if((cast(MenuItem) w) is null)
12034 							continue;
12035 
12036 						if(first is null)
12037 							first = w;
12038 
12039 						if(next !is null) {
12040 							next = w;
12041 							break;
12042 						}
12043 
12044 						if(currentPlace is null) {
12045 							next = w;
12046 							break;
12047 						}
12048 
12049 						if(w is currentPlace) {
12050 							next = w;
12051 						}
12052 					}
12053 
12054 					if(next is currentPlace)
12055 						next = first;
12056 
12057 					changeCurrentPlace(next);
12058 					break;
12059 				case Key.Up:
12060 					Widget prev;
12061 					foreach(w; this.children) {
12062 						if((cast(MenuItem) w) is null)
12063 							continue;
12064 						if(w is currentPlace) {
12065 							if(prev is null) {
12066 								foreach_reverse(c; this.children) {
12067 									if((cast(MenuItem) c) !is null) {
12068 										prev = c;
12069 										break;
12070 									}
12071 								}
12072 							}
12073 							break;
12074 						}
12075 						prev = w;
12076 					}
12077 					changeCurrentPlace(prev);
12078 					break;
12079 				case Key.Left:
12080 				case Key.Right:
12081 					if(menuParent) {
12082 						Menu first;
12083 						Menu last;
12084 						Menu prev;
12085 						Menu next;
12086 						bool found;
12087 
12088 						size_t prev_idx;
12089 						size_t next_idx;
12090 
12091 						MenuBar mb = cast(MenuBar) menuParent.parent;
12092 
12093 						if(mb) {
12094 							foreach(idx, menu; mb.subMenus) {
12095 								if(first is null)
12096 									first = menu;
12097 								last = menu;
12098 								if(found && next is null) {
12099 									next = menu;
12100 									next_idx = idx;
12101 								}
12102 								if(menu is this)
12103 									found = true;
12104 								if(!found) {
12105 									prev = menu;
12106 									prev_idx = idx;
12107 								}
12108 							}
12109 
12110 							Menu nextMenu;
12111 							size_t nextMenuIdx;
12112 							if(ke.key == Key.Left) {
12113 								nextMenu = prev ? prev : last;
12114 								nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1;
12115 							} else {
12116 								nextMenu = next ? next : first;
12117 								nextMenuIdx = next ? next_idx : 0;
12118 							}
12119 
12120 							unpopup();
12121 
12122 							auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right
12123 							rent.dynamicState = DynamicState.depressed | DynamicState.hover;
12124 							nextMenu.popup(rent);
12125 						}
12126 					}
12127 					break;
12128 				case Key.Enter:
12129 				case Key.PadEnter:
12130 					// because the key up and char events will go back to the other window after we unpopup!
12131 					// we will wait for the char event to come (in the following method)
12132 					break;
12133 				case Key.Escape:
12134 					unpopup();
12135 					break;
12136 				default:
12137 			}
12138 		}
12139 		override void defaultEventHandler_char(CharEvent ke) {
12140 			// if one is selected, enter activates it
12141 			if(currentPlace) {
12142 				if(ke.character == '\n') {
12143 					// enter selects
12144 					auto event = new Event(EventType.triggered, currentPlace);
12145 					event.dispatch();
12146 					unpopup();
12147 					return;
12148 				}
12149 			}
12150 
12151 			// otherwise search for a hotkey
12152 			foreach(item; items) {
12153 				if(item.hotkey == ke.character) {
12154 					auto event = new Event(EventType.triggered, item);
12155 					event.dispatch();
12156 					unpopup();
12157 					return;
12158 				}
12159 			}
12160 		}
12161 		override void defaultEventHandler_mouseover(MouseOverEvent moe) {
12162 			if(moe.target && moe.target.parent is this)
12163 				changeCurrentPlace(moe.target);
12164 		}
12165 	}
12166 }
12167 
12168 /++
12169 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
12170 +/
12171 class MenuItem : MouseActivatedWidget {
12172 	Menu submenu;
12173 
12174 	Action action;
12175 	string label;
12176 	dchar hotkey;
12177 
12178 	override int paddingLeft() { return 4; }
12179 
12180 	override int maxHeight() { return defaultLineHeight + 4; }
12181 	override int minHeight() { return defaultLineHeight + 4; }
12182 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
12183 	override int maxWidth() {
12184 		if(cast(MenuBar) parent) {
12185 			return minWidth();
12186 		}
12187 		return int.max;
12188 	}
12189 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
12190 	this(string lbl, Widget parent = null) {
12191 		super(parent);
12192 		//label = lbl; // FIXME
12193 		foreach(idx, char ch; lbl) // FIXME
12194 			if(ch != '&') { // FIXME
12195 				label ~= ch; // FIXME
12196 			} else {
12197 				if(idx + 1 < lbl.length) {
12198 					hotkey = lbl[idx + 1];
12199 					if(hotkey >= 'A' && hotkey <= 'Z')
12200 						hotkey += 32;
12201 				}
12202 			}
12203 		tabStop = false; // these are selected some other way
12204 	}
12205 
12206 	///
12207 	this(Action action, Widget parent = null) {
12208 		assert(action !is null);
12209 		this(action.label, parent);
12210 		this.action = action;
12211 		tabStop = false; // these are selected some other way
12212 	}
12213 
12214 	version(custom_widgets)
12215 	override void paint(WidgetPainter painter) {
12216 		auto cs = getComputedStyle();
12217 		if(dynamicState & DynamicState.depressed)
12218 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12219 		else {
12220 			if(dynamicState & DynamicState.hover) {
12221 				painter.fillColor = cs.hoveringColor;
12222 				painter.outlineColor = Color.transparent;
12223 			} else {
12224 				painter.fillColor = cs.background.color;
12225 				painter.outlineColor = Color.transparent;
12226 			}
12227 
12228 			painter.drawRectangle(Point(0, 0), Size(this.width, this.height));
12229 		}
12230 
12231 		if(dynamicState & DynamicState.hover)
12232 			painter.outlineColor = cs.activeMenuItemColor;
12233 		else
12234 			painter.outlineColor = cs.foregroundColor;
12235 		painter.fillColor = Color.transparent;
12236 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
12237 		if(action && action.accelerator !is KeyEvent.init) {
12238 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
12239 
12240 		}
12241 	}
12242 
12243 	static class Style : Widget.Style {
12244 		override bool variesWithState(ulong dynamicStateFlags) {
12245 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
12246 		}
12247 	}
12248 	mixin OverrideStyle!Style;
12249 
12250 	override void defaultEventHandler_triggered(Event event) {
12251 		if(action)
12252 		foreach(handler; action.triggered)
12253 			handler();
12254 
12255 		if(auto pmenu = cast(Menu) this.parent)
12256 			pmenu.remove();
12257 
12258 		super.defaultEventHandler_triggered(event);
12259 	}
12260 }
12261 
12262 version(win32_widgets)
12263 /// A "mouse activiated widget" is really just an abstract variant of button.
12264 class MouseActivatedWidget : Widget {
12265 	@property bool isChecked() {
12266 		assert(hwnd);
12267 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
12268 
12269 	}
12270 	@property void isChecked(bool state) {
12271 		assert(hwnd);
12272 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
12273 
12274 	}
12275 
12276 	override void handleWmCommand(ushort cmd, ushort id) {
12277 		if(cmd == 0) {
12278 			auto event = new Event(EventType.triggered, this);
12279 			event.dispatch();
12280 		}
12281 	}
12282 
12283 	this(Widget parent) {
12284 		super(parent);
12285 	}
12286 }
12287 else version(custom_widgets)
12288 /// ditto
12289 class MouseActivatedWidget : Widget {
12290 	@property bool isChecked() { return isChecked_; }
12291 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
12292 
12293 	private bool isChecked_;
12294 
12295 	this(Widget parent) {
12296 		super(parent);
12297 
12298 		addEventListener((MouseDownEvent ev) {
12299 			if(ev.button == MouseButton.left) {
12300 				setDynamicState(DynamicState.depressed, true);
12301 				setDynamicState(DynamicState.hover, true);
12302 				redraw();
12303 			}
12304 		});
12305 
12306 		addEventListener((MouseUpEvent ev) {
12307 			if(ev.button == MouseButton.left) {
12308 				setDynamicState(DynamicState.depressed, false);
12309 				setDynamicState(DynamicState.hover, false);
12310 				redraw();
12311 			}
12312 		});
12313 
12314 		addEventListener((MouseMoveEvent mme) {
12315 			if(!(mme.state & ModifierState.leftButtonDown)) {
12316 				if(dynamicState_ & DynamicState.depressed) {
12317 					setDynamicState(DynamicState.depressed, false);
12318 					redraw();
12319 				}
12320 			}
12321 		});
12322 	}
12323 
12324 	override void defaultEventHandler_focus(Event ev) {
12325 		super.defaultEventHandler_focus(ev);
12326 		this.redraw();
12327 	}
12328 	override void defaultEventHandler_blur(Event ev) {
12329 		super.defaultEventHandler_blur(ev);
12330 		setDynamicState(DynamicState.depressed, false);
12331 		this.redraw();
12332 	}
12333 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
12334 		super.defaultEventHandler_keydown(ev);
12335 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
12336 			setDynamicState(DynamicState.depressed, true);
12337 			setDynamicState(DynamicState.hover, true);
12338 			this.redraw();
12339 		}
12340 	}
12341 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
12342 		super.defaultEventHandler_keyup(ev);
12343 		if(!(dynamicState & DynamicState.depressed))
12344 			return;
12345 		setDynamicState(DynamicState.depressed, false);
12346 		setDynamicState(DynamicState.hover, false);
12347 		this.redraw();
12348 
12349 		auto event = new Event(EventType.triggered, this);
12350 		event.sendDirectly();
12351 	}
12352 	override void defaultEventHandler_click(ClickEvent ev) {
12353 		super.defaultEventHandler_click(ev);
12354 		if(ev.button == MouseButton.left) {
12355 			auto event = new Event(EventType.triggered, this);
12356 			event.sendDirectly();
12357 		}
12358 	}
12359 
12360 }
12361 else static assert(false);
12362 
12363 /*
12364 /++
12365 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
12366 
12367 	Basically the same as a checkbox.
12368 +/
12369 class OnOffSwitch : MouseActivatedWidget {
12370 
12371 }
12372 */
12373 
12374 /++
12375 	History:
12376 		Added June 15, 2021 (dub v10.1)
12377 +/
12378 struct ImageLabel {
12379 	/++
12380 		Defines a label+image combo used by some widgets.
12381 
12382 		If you provide just a text label, that is all the widget will try to
12383 		display. Or just an image will display just that. If you provide both,
12384 		it may display both text and image side by side or display the image
12385 		and offer text on an input event depending on the widget.
12386 
12387 		History:
12388 			The `alignment` parameter was added on September 27, 2021
12389 	+/
12390 	this(string label, TextAlignment alignment = TextAlignment.Center) {
12391 		this.label = label;
12392 		this.displayFlags = DisplayFlags.displayText;
12393 		this.alignment = alignment;
12394 	}
12395 
12396 	/// ditto
12397 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
12398 		this.label = label;
12399 		this.image = image;
12400 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
12401 		this.alignment = alignment;
12402 	}
12403 
12404 	/// ditto
12405 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
12406 		this.image = image;
12407 		this.displayFlags = DisplayFlags.displayImage;
12408 		this.alignment = alignment;
12409 	}
12410 
12411 	/// ditto
12412 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
12413 		this.label = label;
12414 		this.image = image;
12415 		this.alignment = alignment;
12416 		this.displayFlags = displayFlags;
12417 	}
12418 
12419 	string label;
12420 	MemoryImage image;
12421 
12422 	enum DisplayFlags {
12423 		displayText = 1 << 0,
12424 		displayImage = 1 << 1,
12425 	}
12426 
12427 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
12428 
12429 	TextAlignment alignment;
12430 }
12431 
12432 /++
12433 	A basic checked or not checked box with an attached label.
12434 
12435 
12436 	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
12437 
12438 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12439 
12440 	History:
12441 		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.
12442 +/
12443 class Checkbox : MouseActivatedWidget {
12444 	version(win32_widgets) {
12445 		override int maxHeight() { return scaleWithDpi(16); }
12446 		override int minHeight() { return scaleWithDpi(16); }
12447 	} else version(custom_widgets) {
12448 		private enum buttonSize = 16;
12449 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
12450 		override int minHeight() { return maxHeight(); }
12451 	} else static assert(0);
12452 
12453 	override int marginLeft() { return 4; }
12454 
12455 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
12456 
12457 	/++
12458 		Just an alias because I keep typing checked out of web habit.
12459 
12460 		History:
12461 			Added May 31, 2021
12462 	+/
12463 	alias checked = isChecked;
12464 
12465 	private string label;
12466 	private dchar accelerator;
12467 
12468 	/++
12469 	+/
12470 	this(string label, Widget parent) {
12471 		this(ImageLabel(label), Appearance.checkbox, parent);
12472 	}
12473 
12474 	/// ditto
12475 	this(string label, Appearance appearance, Widget parent) {
12476 		this(ImageLabel(label), appearance, parent);
12477 	}
12478 
12479 	/++
12480 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
12481 
12482 		History:
12483 			Added June 29, 2021 (dub v10.2)
12484 	+/
12485 	enum Appearance {
12486 		checkbox, /// a normal checkbox
12487 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
12488 		//sliderswitch,
12489 	}
12490 	private Appearance appearance;
12491 
12492 	/// ditto
12493 	private this(ImageLabel label, Appearance appearance, Widget parent) {
12494 		super(parent);
12495 		version(win32_widgets) {
12496 			this.label = label.label;
12497 
12498 			uint extraStyle;
12499 			final switch(appearance) {
12500 				case Appearance.checkbox:
12501 				break;
12502 				case Appearance.pushbutton:
12503 					extraStyle |= BS_PUSHLIKE;
12504 				break;
12505 			}
12506 
12507 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
12508 		} else version(custom_widgets) {
12509 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
12510 		} else static assert(0);
12511 	}
12512 
12513 	version(custom_widgets)
12514 	override void paint(WidgetPainter painter) {
12515 		auto cs = getComputedStyle();
12516 		if(isFocused()) {
12517 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
12518 			painter.fillColor = cs.windowBackgroundColor;
12519 			painter.drawRectangle(Point(0, 0), width, height);
12520 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
12521 		} else {
12522 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
12523 			painter.fillColor = cs.windowBackgroundColor;
12524 			painter.drawRectangle(Point(0, 0), width, height);
12525 		}
12526 
12527 
12528 		painter.outlineColor = Color.black;
12529 		painter.fillColor = Color.white;
12530 		enum rectOffset = 2;
12531 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
12532 
12533 		if(isChecked) {
12534 			auto size = scaleWithDpi(2);
12535 			painter.pen = Pen(Color.black, size);
12536 			// I'm using height so the checkbox is square
12537 			enum padding = 3;
12538 			painter.drawLine(
12539 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
12540 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
12541 			);
12542 			painter.drawLine(
12543 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
12544 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
12545 			);
12546 
12547 			painter.pen = Pen(Color.black, 1);
12548 		}
12549 
12550 		if(label !is null) {
12551 			painter.outlineColor = cs.foregroundColor();
12552 			painter.fillColor = cs.foregroundColor();
12553 
12554 			// i want the centerline of the text to be aligned with the centerline of the checkbox
12555 			/+
12556 			auto font = cs.font();
12557 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
12558 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
12559 			+/
12560 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
12561 		}
12562 	}
12563 
12564 	override void defaultEventHandler_triggered(Event ev) {
12565 		isChecked = !isChecked;
12566 
12567 		this.emit!(ChangeEvent!bool)(&isChecked);
12568 
12569 		redraw();
12570 	}
12571 
12572 	/// Emits a change event with the checked state
12573 	mixin Emits!(ChangeEvent!bool);
12574 }
12575 
12576 /// Adds empty space to a layout.
12577 class VerticalSpacer : Widget {
12578 	private int mh;
12579 
12580 	/++
12581 		History:
12582 			The overload with `maxHeight` was added on December 31, 2024
12583 	+/
12584 	this(Widget parent) {
12585 		this(0, parent);
12586 	}
12587 
12588 	/// ditto
12589 	this(int maxHeight, Widget parent) {
12590 		this.mh = maxHeight;
12591 		super(parent);
12592 		this.tabStop = false;
12593 	}
12594 
12595 	override int maxHeight() {
12596 		return mh ? scaleWithDpi(mh) : super.maxHeight();
12597 	}
12598 }
12599 
12600 
12601 /// ditto
12602 class HorizontalSpacer : Widget {
12603 	private int mw;
12604 
12605 	/++
12606 		History:
12607 			The overload with `maxWidth` was added on December 31, 2024
12608 	+/
12609 	this(Widget parent) {
12610 		this(0, parent);
12611 	}
12612 
12613 	/// ditto
12614 	this(int maxWidth, Widget parent) {
12615 		this.mw = maxWidth;
12616 		super(parent);
12617 		this.tabStop = false;
12618 	}
12619 
12620 	override int maxWidth() {
12621 		return mw ? scaleWithDpi(mw) : super.maxWidth();
12622 	}
12623 }
12624 
12625 
12626 /++
12627 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
12628 
12629 
12630 	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
12631 
12632 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12633 
12634 	History:
12635 		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.
12636 +/
12637 class Radiobox : MouseActivatedWidget {
12638 
12639 	version(win32_widgets) {
12640 		override int maxHeight() { return scaleWithDpi(16); }
12641 		override int minHeight() { return scaleWithDpi(16); }
12642 	} else version(custom_widgets) {
12643 		private enum buttonSize = 16;
12644 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
12645 		override int minHeight() { return maxHeight(); }
12646 	} else static assert(0);
12647 
12648 	override int marginLeft() { return 4; }
12649 
12650 	// FIXME: make a label getter
12651 	private string label;
12652 	private dchar accelerator;
12653 
12654 	/++
12655 
12656 	+/
12657 	this(string label, Widget parent) {
12658 		super(parent);
12659 		version(win32_widgets) {
12660 			this.label = label;
12661 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
12662 		} else version(custom_widgets) {
12663 			label.extractWindowsStyleLabel(this.label, this.accelerator);
12664 			height = 16;
12665 			width = height + 4 + cast(int) label.length * 16;
12666 		}
12667 	}
12668 
12669 	version(custom_widgets)
12670 	override void paint(WidgetPainter painter) {
12671 		auto cs = getComputedStyle();
12672 
12673 		if(isFocused) {
12674 			painter.fillColor = cs.windowBackgroundColor;
12675 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
12676 		} else {
12677 			painter.fillColor = cs.windowBackgroundColor;
12678 			painter.outlineColor = cs.windowBackgroundColor;
12679 		}
12680 		painter.drawRectangle(Point(0, 0), width, height);
12681 
12682 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
12683 
12684 		painter.outlineColor = Color.black;
12685 		painter.fillColor = Color.white;
12686 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
12687 		if(isChecked) {
12688 			painter.outlineColor = Color.black;
12689 			painter.fillColor = Color.black;
12690 			// I'm using height so the checkbox is square
12691 			auto size = scaleWithDpi(2);
12692 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
12693 		}
12694 
12695 		painter.outlineColor = cs.foregroundColor();
12696 		painter.fillColor = cs.foregroundColor();
12697 
12698 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
12699 	}
12700 
12701 
12702 	override void defaultEventHandler_triggered(Event ev) {
12703 		isChecked = true;
12704 
12705 		if(this.parent) {
12706 			foreach(child; this.parent.children) {
12707 				if(child is this) continue;
12708 				if(auto rb = cast(Radiobox) child) {
12709 					rb.isChecked = false;
12710 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
12711 					rb.redraw();
12712 				}
12713 			}
12714 		}
12715 
12716 		this.emit!(ChangeEvent!bool)(&this.isChecked);
12717 
12718 		redraw();
12719 	}
12720 
12721 	/// 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.
12722 	mixin Emits!(ChangeEvent!bool);
12723 }
12724 
12725 
12726 /++
12727 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
12728 
12729 
12730 	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
12731 
12732 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12733 
12734 	History:
12735 		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.
12736 +/
12737 class Button : MouseActivatedWidget {
12738 	override int heightStretchiness() { return 3; }
12739 	override int widthStretchiness() { return 3; }
12740 
12741 	/++
12742 		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.
12743 
12744 		History:
12745 			Added July 2, 2021
12746 	+/
12747 	public bool triggersOnMultiClick;
12748 
12749 	private string label_;
12750 	private TextAlignment alignment;
12751 	private dchar accelerator;
12752 
12753 	///
12754 	string label() { return label_; }
12755 	///
12756 	void label(string l) {
12757 		label_ = l;
12758 		version(win32_widgets) {
12759 			WCharzBuffer bfr = WCharzBuffer(l);
12760 			SetWindowTextW(hwnd, bfr.ptr);
12761 		} else version(custom_widgets) {
12762 			redraw();
12763 		}
12764 	}
12765 
12766 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
12767 		super.defaultEventHandler_dblclick(ev);
12768 		if(triggersOnMultiClick) {
12769 			if(ev.button == MouseButton.left) {
12770 				auto event = new Event(EventType.triggered, this);
12771 				event.sendDirectly();
12772 			}
12773 		}
12774 	}
12775 
12776 	private Sprite sprite;
12777 	private int displayFlags;
12778 
12779 	/++
12780 		Creates a push button with the given label, which may be an image or some text.
12781 
12782 		Bugs:
12783 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
12784 
12785 		History:
12786 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
12787 
12788 			The button with label and image will respect requests to show both on Windows as
12789 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
12790 	+/
12791 	this(ImageLabel label, Widget parent) {
12792 		version(win32_widgets) {
12793 			// FIXME: use ideal button size instead
12794 			width = 50;
12795 			height = 30;
12796 			super(parent);
12797 
12798 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
12799 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
12800 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
12801 
12802 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
12803 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
12804 
12805 			if(label.image) {
12806 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
12807 
12808 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
12809 			}
12810 
12811 			this.label = label.label;
12812 		} else version(custom_widgets) {
12813 			width = 50;
12814 			height = 30;
12815 			super(parent);
12816 
12817 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
12818 
12819 			if(label.image) {
12820 				this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
12821 				this.displayFlags = label.displayFlags;
12822 			}
12823 
12824 			this.alignment = label.alignment;
12825 		}
12826 	}
12827 
12828 	///
12829 	this(string label, Widget parent) {
12830 		this(ImageLabel(label), parent);
12831 	}
12832 
12833 	override int minHeight() { return defaultLineHeight + 4; }
12834 
12835 	static class Style : Widget.Style {
12836 		override WidgetBackground background() {
12837 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
12838 
12839 			auto pressed = DynamicState.depressed | DynamicState.hover;
12840 			if((widget.dynamicState & pressed) == pressed) {
12841 				return WidgetBackground(cs.depressedButtonColor());
12842 			} else if(widget.dynamicState & DynamicState.hover) {
12843 				return WidgetBackground(cs.hoveringColor());
12844 			} else {
12845 				return WidgetBackground(cs.buttonColor());
12846 			}
12847 		}
12848 
12849 		override FrameStyle borderStyle() {
12850 			auto pressed = DynamicState.depressed | DynamicState.hover;
12851 			if((widget.dynamicState & pressed) == pressed) {
12852 				return FrameStyle.sunk;
12853 			} else {
12854 				return FrameStyle.risen;
12855 			}
12856 
12857 		}
12858 
12859 		override bool variesWithState(ulong dynamicStateFlags) {
12860 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
12861 		}
12862 	}
12863 	mixin OverrideStyle!Style;
12864 
12865 	version(custom_widgets)
12866 	override void paint(WidgetPainter painter) {
12867 		painter.drawThemed(delegate Rectangle(const Rectangle bounds) {
12868 			if(sprite) {
12869 				sprite.drawAt(
12870 					painter,
12871 					bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
12872 					Point(0, 0)
12873 				);
12874 			} else {
12875 				painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
12876 			}
12877 			return bounds;
12878 		});
12879 	}
12880 
12881 	override int flexBasisWidth() {
12882 		version(win32_widgets) {
12883 			SIZE size;
12884 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
12885 			if(size.cx == 0)
12886 				goto fallback;
12887 			return size.cx + scaleWithDpi(16);
12888 		}
12889 		fallback:
12890 			return scaleWithDpi(cast(int) label.length * 8 + 16);
12891 	}
12892 
12893 	override int flexBasisHeight() {
12894 		version(win32_widgets) {
12895 			SIZE size;
12896 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
12897 			if(size.cy == 0)
12898 				goto fallback;
12899 			return size.cy + scaleWithDpi(6);
12900 		}
12901 		fallback:
12902 			return defaultLineHeight + 4;
12903 	}
12904 }
12905 
12906 /++
12907 	A button with a consistent size, suitable for user commands like OK and CANCEL.
12908 +/
12909 class CommandButton : Button {
12910 	this(string label, Widget parent) {
12911 		super(label, parent);
12912 	}
12913 
12914 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
12915 
12916 	override int maxHeight() {
12917 		return defaultLineHeight + 4;
12918 	}
12919 
12920 	override int maxWidth() {
12921 		return defaultLineHeight * 4;
12922 	}
12923 
12924 	override int marginLeft() { return 12; }
12925 	override int marginRight() { return 12; }
12926 	override int marginTop() { return 12; }
12927 	override int marginBottom() { return 12; }
12928 }
12929 
12930 ///
12931 enum ArrowDirection {
12932 	left, ///
12933 	right, ///
12934 	up, ///
12935 	down ///
12936 }
12937 
12938 ///
12939 version(custom_widgets)
12940 class ArrowButton : Button {
12941 	///
12942 	this(ArrowDirection direction, Widget parent) {
12943 		super("", parent);
12944 		this.direction = direction;
12945 		triggersOnMultiClick = true;
12946 	}
12947 
12948 	private ArrowDirection direction;
12949 
12950 	override int minHeight() { return scaleWithDpi(16); }
12951 	override int maxHeight() { return scaleWithDpi(16); }
12952 	override int minWidth() { return scaleWithDpi(16); }
12953 	override int maxWidth() { return scaleWithDpi(16); }
12954 
12955 	override void paint(WidgetPainter painter) {
12956 		super.paint(painter);
12957 
12958 		auto cs = getComputedStyle();
12959 
12960 		painter.outlineColor = cs.foregroundColor;
12961 		painter.fillColor = cs.foregroundColor;
12962 
12963 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
12964 
12965 		final switch(direction) {
12966 			case ArrowDirection.up:
12967 				painter.drawPolygon(
12968 					scaleWithDpi(Point(2, 10) + offset),
12969 					scaleWithDpi(Point(7, 5) + offset),
12970 					scaleWithDpi(Point(12, 10) + offset),
12971 					scaleWithDpi(Point(2, 10) + offset)
12972 				);
12973 			break;
12974 			case ArrowDirection.down:
12975 				painter.drawPolygon(
12976 					scaleWithDpi(Point(2, 6) + offset),
12977 					scaleWithDpi(Point(7, 11) + offset),
12978 					scaleWithDpi(Point(12, 6) + offset),
12979 					scaleWithDpi(Point(2, 6) + offset)
12980 				);
12981 			break;
12982 			case ArrowDirection.left:
12983 				painter.drawPolygon(
12984 					scaleWithDpi(Point(10, 2) + offset),
12985 					scaleWithDpi(Point(5, 7) + offset),
12986 					scaleWithDpi(Point(10, 12) + offset),
12987 					scaleWithDpi(Point(10, 2) + offset)
12988 				);
12989 			break;
12990 			case ArrowDirection.right:
12991 				painter.drawPolygon(
12992 					scaleWithDpi(Point(6, 2) + offset),
12993 					scaleWithDpi(Point(11, 7) + offset),
12994 					scaleWithDpi(Point(6, 12) + offset),
12995 					scaleWithDpi(Point(6, 2) + offset)
12996 				);
12997 			break;
12998 		}
12999 	}
13000 }
13001 
13002 private
13003 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
13004 	int x, y;
13005 	Widget par = c;
13006 	while(par) {
13007 		x += par.x;
13008 		y += par.y;
13009 		par = par.parent;
13010 	}
13011 	return [x, y];
13012 }
13013 
13014 version(win32_widgets)
13015 private
13016 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
13017 // MapWindowPoints?
13018 	int x, y;
13019 	Widget par = c;
13020 	while(par) {
13021 		x += par.x;
13022 		y += par.y;
13023 		par = par.parent;
13024 		if(par !is null && par.useNativeDrawing())
13025 			break;
13026 	}
13027 	return [x, y];
13028 }
13029 
13030 ///
13031 class ImageBox : Widget {
13032 	private MemoryImage image_;
13033 
13034 	override int widthStretchiness() { return 1; }
13035 	override int heightStretchiness() { return 1; }
13036 	override int widthShrinkiness() { return 1; }
13037 	override int heightShrinkiness() { return 1; }
13038 
13039 	override int flexBasisHeight() {
13040 		return image_.height;
13041 	}
13042 
13043 	override int flexBasisWidth() {
13044 		return image_.width;
13045 	}
13046 
13047 	///
13048 	public void setImage(MemoryImage image){
13049 		this.image_ = image;
13050 		if(this.parentWindow && this.parentWindow.win) {
13051 			if(sprite)
13052 				sprite.dispose();
13053 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13054 		}
13055 		redraw();
13056 	}
13057 
13058 	/// How to fit the image in the box if they aren't an exact match in size?
13059 	enum HowToFit {
13060 		center, /// centers the image, cropping around all the edges as needed
13061 		crop, /// always draws the image in the upper left, cropping the lower right if needed
13062 		// stretch, /// not implemented
13063 	}
13064 
13065 	private Sprite sprite;
13066 	private HowToFit howToFit_;
13067 
13068 	private Color backgroundColor_;
13069 
13070 	///
13071 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
13072 		this.image_ = image;
13073 		this.tabStop = false;
13074 		this.howToFit_ = howToFit;
13075 		this.backgroundColor_ = backgroundColor;
13076 		super(parent);
13077 		updateSprite();
13078 	}
13079 
13080 	/// ditto
13081 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
13082 		this(image, howToFit, Color.transparent, parent);
13083 	}
13084 
13085 	private void updateSprite() {
13086 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
13087 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13088 		}
13089 	}
13090 
13091 	override void paint(WidgetPainter painter) {
13092 		updateSprite();
13093 		if(backgroundColor_.a) {
13094 			painter.fillColor = backgroundColor_;
13095 			painter.drawRectangle(Point(0, 0), width, height);
13096 		}
13097 		if(howToFit_ == HowToFit.crop)
13098 			sprite.drawAt(painter, Point(0, 0));
13099 		else if(howToFit_ == HowToFit.center) {
13100 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
13101 		}
13102 	}
13103 }
13104 
13105 ///
13106 class TextLabel : Widget {
13107 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
13108 	override int maxHeight() { return minHeight; }
13109 	override int minWidth() { return 32; }
13110 
13111 	override int flexBasisHeight() { return minHeight(); }
13112 	override int flexBasisWidth() { return defaultTextWidth(label); }
13113 
13114 	string label_;
13115 
13116 	/++
13117 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
13118 
13119 		In practice this means a click on the label will focus the `labelFor`. In future versions
13120 		it will also set screen reader hints but that is not yet implemented.
13121 
13122 		History:
13123 			Added October 3, 2021 (dub v10.4)
13124 	+/
13125 	Widget labelFor;
13126 
13127 	///
13128 	@scriptable
13129 	string label() { return label_; }
13130 
13131 	///
13132 	@scriptable
13133 	void label(string l) {
13134 		label_ = l;
13135 		version(win32_widgets) {
13136 			WCharzBuffer bfr = WCharzBuffer(l);
13137 			SetWindowTextW(hwnd, bfr.ptr);
13138 		} else version(custom_widgets)
13139 			redraw();
13140 	}
13141 
13142 	override void defaultEventHandler_click(scope ClickEvent ce) {
13143 		if(this.labelFor !is null)
13144 			this.labelFor.focus();
13145 	}
13146 
13147 	/++
13148 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
13149 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
13150 	+/
13151 	this(string label, TextAlignment alignment, Widget parent) {
13152 		this.label_ = label;
13153 		this.alignment = alignment;
13154 		this.tabStop = false;
13155 		super(parent);
13156 
13157 		version(win32_widgets)
13158 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
13159 	}
13160 
13161 	/// ditto
13162 	this(string label, Widget parent) {
13163 		this(label, TextAlignment.Right, parent);
13164 	}
13165 
13166 	TextAlignment alignment;
13167 
13168 	version(custom_widgets)
13169 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13170 		painter.outlineColor = getComputedStyle().foregroundColor;
13171 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
13172 		return bounds;
13173 	}
13174 }
13175 
13176 class TextDisplayHelper : Widget {
13177 	protected TextLayouter l;
13178 	protected ScrollMessageWidget smw;
13179 
13180 	private const(TextLayouter.State)*[] undoStack;
13181 	private const(TextLayouter.State)*[] redoStack;
13182 
13183 	private string preservedPrimaryText;
13184 	protected void selectionChanged() {
13185 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
13186 		static if(UsingSimpledisplayX11)
13187 		with(l.selection()) {
13188 			if(!isEmpty()) {
13189 				//sdpyPrintDebugString("!isEmpty");
13190 
13191 				getPrimarySelection(parentWindow.win, (in char[] txt) {
13192 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
13193 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
13194 					if(txt.length) {
13195 						preservedPrimaryText = txt.idup;
13196 						// writeln(preservedPrimaryText);
13197 					}
13198 
13199 					setPrimarySelection(parentWindow.win, getContentString());
13200 				});
13201 			}
13202 		}
13203 	}
13204 
13205 	final TextLayouter layouter() {
13206 		return l;
13207 	}
13208 
13209 	bool readonly;
13210 	bool caretNavigation; // scroll lock can flip this
13211 	bool singleLine;
13212 	bool acceptsTabInput;
13213 
13214 	private Menu ctx;
13215 	override Menu contextMenu(int x, int y) {
13216 		if(ctx is null) {
13217 			ctx = new Menu("Actions", this);
13218 			if(!readonly) {
13219 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
13220 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
13221 				ctx.addSeparator();
13222 			}
13223 			if(!readonly)
13224 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
13225 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
13226 			if(!readonly)
13227 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
13228 			if(!readonly)
13229 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
13230 			ctx.addSeparator();
13231 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
13232 		}
13233 		return ctx;
13234 	}
13235 
13236 	override void defaultEventHandler_blur(Event ev) {
13237 		super.defaultEventHandler_blur(ev);
13238 		if(l.wasMutated()) {
13239 			auto evt = new ChangeEvent!string(this, &this.content);
13240 			evt.dispatch();
13241 			l.clearWasMutatedFlag();
13242 		}
13243 	}
13244 
13245 	private string content() {
13246 		return l.getTextString();
13247 	}
13248 
13249 	void undo() {
13250 		if(readonly) return;
13251 		if(undoStack.length) {
13252 			auto state = undoStack[$-1];
13253 			undoStack = undoStack[0 .. $-1];
13254 			undoStack.assumeSafeAppend();
13255 			redoStack ~= l.saveState();
13256 			l.restoreState(state);
13257 			adjustScrollbarSizes();
13258 			scrollForCaret();
13259 			redraw();
13260 			stateCheckpoint = true;
13261 		}
13262 	}
13263 
13264 	void redo() {
13265 		if(readonly) return;
13266 		if(redoStack.length) {
13267 			doStateCheckpoint();
13268 			auto state = redoStack[$-1];
13269 			redoStack = redoStack[0 .. $-1];
13270 			redoStack.assumeSafeAppend();
13271 			l.restoreState(state);
13272 			adjustScrollbarSizes();
13273 			scrollForCaret();
13274 			redraw();
13275 			stateCheckpoint = true;
13276 		}
13277 	}
13278 
13279 	void cut() {
13280 		if(readonly) return;
13281 		with(l.selection()) {
13282 			if(!isEmpty()) {
13283 				setClipboardText(parentWindow.win, getContentString());
13284 				doStateCheckpoint();
13285 				replaceContent("");
13286 				adjustScrollbarSizes();
13287 				scrollForCaret();
13288 				this.redraw();
13289 			}
13290 		}
13291 
13292 	}
13293 
13294 	void copy() {
13295 		with(l.selection()) {
13296 			if(!isEmpty()) {
13297 				setClipboardText(parentWindow.win, getContentString());
13298 				this.redraw();
13299 			}
13300 		}
13301 	}
13302 
13303 	void paste() {
13304 		if(readonly) return;
13305 		getClipboardText(parentWindow.win, (txt) {
13306 			doStateCheckpoint();
13307 			if(singleLine)
13308 				l.selection.replaceContent(txt.stripInternal());
13309 			else
13310 				l.selection.replaceContent(txt);
13311 			adjustScrollbarSizes();
13312 			scrollForCaret();
13313 			this.redraw();
13314 		});
13315 	}
13316 
13317 	void deleteContentOfSelection() {
13318 		if(readonly) return;
13319 		doStateCheckpoint();
13320 		l.selection.replaceContent("");
13321 		l.selection.setUserXCoordinate();
13322 		adjustScrollbarSizes();
13323 		scrollForCaret();
13324 		redraw();
13325 	}
13326 
13327 	void selectAll() {
13328 		with(l.selection) {
13329 			moveToStartOfDocument();
13330 			setAnchor();
13331 			moveToEndOfDocument();
13332 			setFocus();
13333 
13334 			selectionChanged();
13335 		}
13336 		redraw();
13337 	}
13338 
13339 	protected bool stateCheckpoint = true;
13340 
13341 	protected void doStateCheckpoint() {
13342 		if(stateCheckpoint) {
13343 			undoStack ~= l.saveState();
13344 			stateCheckpoint = false;
13345 		}
13346 	}
13347 
13348 	protected void adjustScrollbarSizes() {
13349 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
13350 		auto borderWidth = 2;
13351 		this.smw.setTotalArea(l.width, l.height);
13352 		this.smw.setViewableArea(
13353 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
13354 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
13355 	}
13356 
13357 	protected void scrollForCaret() {
13358 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
13359 		smw.scrollIntoView(l.selection.focusBoundingBox());
13360 	}
13361 
13362 	// FIXME: this should be a theme changed event listener instead
13363 	private BaseVisualTheme currentTheme;
13364 	override void recomputeChildLayout() {
13365 		if(currentTheme is null)
13366 			currentTheme = WidgetPainter.visualTheme;
13367 		if(WidgetPainter.visualTheme !is currentTheme) {
13368 			currentTheme = WidgetPainter.visualTheme;
13369 			auto ds = this.l.defaultStyle;
13370 			if(auto ms = cast(MyTextStyle) ds) {
13371 				auto cs = getComputedStyle();
13372 				auto font = cs.font();
13373 				if(font !is null)
13374 					ms.font_ = font;
13375 				else {
13376 					auto osc = new OperatingSystemFont();
13377 					osc.loadDefault;
13378 					ms.font_ = osc;
13379 				}
13380 			}
13381 		}
13382 		super.recomputeChildLayout();
13383 	}
13384 
13385 	private Point adjustForSingleLine(Point p) {
13386 		if(singleLine)
13387 			return Point(p.x, this.height / 2);
13388 		else
13389 			return p;
13390 	}
13391 
13392 	private bool wordWrapEnabled_;
13393 
13394 	this(TextLayouter l, ScrollMessageWidget parent) {
13395 		this.smw = parent;
13396 
13397 		smw.addDefaultWheelListeners(16, 16, 8);
13398 		smw.movementPerButtonClick(16, 16);
13399 
13400 		this.defaultPadding = Rectangle(2, 2, 2, 2);
13401 
13402 		this.l = l;
13403 		super(parent);
13404 
13405 		smw.addEventListener((scope ScrollEvent se) {
13406 			this.redraw();
13407 		});
13408 
13409 		bool mouseDown;
13410 		bool mouseActuallyMoved;
13411 
13412 		this.addEventListener((scope ResizeEvent re) {
13413 			// FIXME: I should add a method to give this client area width thing
13414 			if(wordWrapEnabled_)
13415 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
13416 
13417 			adjustScrollbarSizes();
13418 			scrollForCaret();
13419 
13420 			this.redraw();
13421 		});
13422 
13423 		this.addEventListener((scope KeyDownEvent kde) {
13424 			switch(kde.key) {
13425 				case Key.Up, Key.Down, Key.Left, Key.Right:
13426 				case Key.Home, Key.End:
13427 					stateCheckpoint = true;
13428 					bool setPosition = false;
13429 					switch(kde.key) {
13430 						case Key.Up: l.selection.moveUp(); break;
13431 						case Key.Down: l.selection.moveDown(); break;
13432 						case Key.Left: l.selection.moveLeft(); setPosition = true; break;
13433 						case Key.Right: l.selection.moveRight(); setPosition = true; break;
13434 						case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
13435 						case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
13436 						default: assert(0);
13437 					}
13438 
13439 					if(kde.shiftKey)
13440 						l.selection.setFocus();
13441 					else
13442 						l.selection.setAnchor();
13443 
13444 					selectionChanged();
13445 
13446 					if(setPosition)
13447 						l.selection.setUserXCoordinate();
13448 					scrollForCaret();
13449 					redraw();
13450 				break;
13451 				case Key.PageUp, Key.PageDown:
13452 					// FIXME
13453 					scrollForCaret();
13454 				break;
13455 				case Key.Delete:
13456 					if(l.selection.isEmpty()) {
13457 						l.selection.setAnchor();
13458 						l.selection.moveRight();
13459 						l.selection.setFocus();
13460 					}
13461 					deleteContentOfSelection();
13462 					adjustScrollbarSizes();
13463 					scrollForCaret();
13464 				break;
13465 				case Key.Insert:
13466 				break;
13467 				case Key.A:
13468 					if(kde.ctrlKey)
13469 						selectAll();
13470 				break;
13471 				case Key.F:
13472 					// find
13473 				break;
13474 				case Key.Z:
13475 					if(kde.ctrlKey)
13476 						undo();
13477 				break;
13478 				case Key.R:
13479 					if(kde.ctrlKey)
13480 						redo();
13481 				break;
13482 				case Key.X:
13483 					if(kde.ctrlKey)
13484 						cut();
13485 				break;
13486 				case Key.C:
13487 					if(kde.ctrlKey)
13488 						copy();
13489 				break;
13490 				case Key.V:
13491 					if(kde.ctrlKey)
13492 						paste();
13493 				break;
13494 				case Key.F1:
13495 					with(l.selection()) {
13496 						moveToStartOfLine();
13497 						setAnchor();
13498 						moveToEndOfLine();
13499 						moveToIncludeAdjacentEndOfLineMarker();
13500 						setFocus();
13501 						replaceContent("");
13502 					}
13503 
13504 					redraw();
13505 				break;
13506 				/*
13507 				case Key.F2:
13508 					l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
13509 						//(cast(MyTextStyle) old).font,
13510 						font2,
13511 						Color.red)));
13512 					redraw();
13513 				break;
13514 				*/
13515 				case Key.Tab:
13516 					// we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl
13517 					if(acceptsTabInput && !kde.ctrlKey)
13518 						kde.preventDefault();
13519 				break;
13520 				default:
13521 			}
13522 		});
13523 
13524 		Point downAt;
13525 
13526 		static if(UsingSimpledisplayX11)
13527 		this.addEventListener((scope ClickEvent ce) {
13528 			if(ce.button == MouseButton.middle) {
13529 				parentWindow.win.getPrimarySelection((txt) {
13530 					doStateCheckpoint();
13531 
13532 					// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
13533 
13534 					if(txt == l.selection.getContentString && preservedPrimaryText.length)
13535 						l.selection.replaceContent(preservedPrimaryText);
13536 					else
13537 						l.selection.replaceContent(txt);
13538 					redraw();
13539 				});
13540 			}
13541 		});
13542 
13543 		this.addEventListener((scope DoubleClickEvent dce) {
13544 			if(dce.button == MouseButton.left) {
13545 				with(l.selection()) {
13546 					scope dg = delegate const(char)[] (scope return const(char)[] ch) {
13547 						if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
13548 							return ch;
13549 						return null;
13550 					};
13551 					find(dg, 1, true).moveToEnd.setAnchor;
13552 					find(dg, 1, false).moveTo.setFocus;
13553 					selectionChanged();
13554 					redraw();
13555 				}
13556 			}
13557 		});
13558 
13559 		this.addEventListener((scope MouseDownEvent ce) {
13560 			if(ce.button == MouseButton.left) {
13561 				downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
13562 				l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
13563 				l.selection.setAnchor();
13564 				mouseDown = true;
13565 				mouseActuallyMoved = false;
13566 				parentWindow.captureMouse(this);
13567 				this.redraw();
13568 			}
13569 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
13570 		});
13571 
13572 		Timer autoscrollTimer;
13573 		int autoscrollDirection;
13574 		int autoscrollAmount;
13575 
13576 		void autoscroll() {
13577 			switch(autoscrollDirection) {
13578 				case 0: smw.scrollUp(autoscrollAmount); break;
13579 				case 1: smw.scrollDown(autoscrollAmount); break;
13580 				case 2: smw.scrollLeft(autoscrollAmount); break;
13581 				case 3: smw.scrollRight(autoscrollAmount); break;
13582 				default: assert(0);
13583 			}
13584 
13585 			this.redraw();
13586 		}
13587 
13588 		void setAutoscrollTimer(int direction, int amount) {
13589 			if(autoscrollTimer is null) {
13590 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
13591 			}
13592 
13593 			autoscrollDirection = direction;
13594 			autoscrollAmount = amount;
13595 		}
13596 
13597 		void stopAutoscrollTimer() {
13598 			if(autoscrollTimer !is null) {
13599 				autoscrollTimer.dispose();
13600 				autoscrollTimer = null;
13601 			}
13602 			autoscrollAmount = 0;
13603 			autoscrollDirection = 0;
13604 		}
13605 
13606 		this.addEventListener((scope MouseMoveEvent ce) {
13607 			if(mouseDown) {
13608 				auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
13609 
13610 				// FIXME: when scrolling i actually do want a timer.
13611 				// i also want a zone near the sides of the window where i can auto scroll
13612 
13613 				auto scrollMultiplier = scaleWithDpi(16);
13614 				auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
13615 
13616 				if(!singleLine && movedTo.y < 4) {
13617 					setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
13618 				} else
13619 				if(!singleLine && (movedTo.y + 6) > this.height) {
13620 					setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
13621 				} else
13622 				if(movedTo.x < 4) {
13623 					setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
13624 				} else
13625 				if((movedTo.x + 6) > this.width) {
13626 					setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
13627 				} else
13628 					stopAutoscrollTimer();
13629 
13630 				l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
13631 				l.selection.setFocus();
13632 				mouseActuallyMoved = true;
13633 				this.redraw();
13634 			}
13635 		});
13636 
13637 		this.addEventListener((scope MouseUpEvent ce) {
13638 			// FIXME: assert primary selection
13639 			if(mouseDown && ce.button == MouseButton.left) {
13640 				stateCheckpoint = true;
13641 				//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
13642 				//l.selection.setFocus();
13643 				mouseDown = false;
13644 				parentWindow.releaseMouseCapture();
13645 				stopAutoscrollTimer();
13646 				this.redraw();
13647 
13648 				if(mouseActuallyMoved)
13649 					selectionChanged();
13650 			}
13651 			//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
13652 		});
13653 
13654 		this.addEventListener((scope CharEvent ce) {
13655 			if(readonly)
13656 				return;
13657 			if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
13658 				return; // skip the ctrl+x characters we don't care about as plain text
13659 
13660 			if(singleLine && ce.character == '\n')
13661 				return;
13662 			if(!acceptsTabInput && ce.character == '\t')
13663 				return;
13664 
13665 			doStateCheckpoint();
13666 
13667 			char[4] buffer;
13668 			import arsd.core;
13669 			auto stride = encodeUtf8(buffer, ce.character);
13670 			l.selection.replaceContent(buffer[0 .. stride]);
13671 			l.selection.setUserXCoordinate();
13672 			adjustScrollbarSizes();
13673 			scrollForCaret();
13674 			redraw();
13675 		});
13676 	}
13677 
13678 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
13679 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
13680 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent
13681 		if(parent && parent.parent && parent.parent.parent)
13682 			parent.parent.parent.useStyleProperties(dg);
13683 		else
13684 			super.useStyleProperties(dg);
13685 	}
13686 
13687 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
13688 	override int maxHeight() {
13689 		if(singleLine)
13690 			return minHeight;
13691 		else
13692 			return super.maxHeight();
13693 	}
13694 
13695 	void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
13696 		painter.drawText(upperLeft, text);
13697 	}
13698 
13699 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13700 		//painter.setFont(font);
13701 
13702 		auto cs = getComputedStyle();
13703 		auto defaultColor = cs.foregroundColor;
13704 
13705 		auto old = painter.setClipRectangle(bounds);
13706 		scope(exit) painter.setClipRectangle(old);
13707 
13708 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
13709 			//writeln("Segment: ", txt);
13710 			assert(style !is null);
13711 
13712 			auto myStyle = cast(MyTextStyle) style;
13713 			assert(myStyle !is null);
13714 
13715 			painter.setFont(myStyle.font);
13716 			// defaultColor = myStyle.color; // FIXME: so wrong
13717 
13718 			if(info.selections && info.boundingBox.width > 0) {
13719 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
13720 				painter.fillColor = color;
13721 				painter.outlineColor = color;
13722 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
13723 				painter.outlineColor = cs.selectionForegroundColor;
13724 				//painter.fillColor = Color.white;
13725 			} else {
13726 				painter.outlineColor = defaultColor;
13727 			}
13728 
13729 			if(this.isFocused)
13730 			foreach(idx, caret; carets) {
13731 				if(idx == 0)
13732 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
13733 				painter.drawLine(
13734 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
13735 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
13736 				);
13737 			}
13738 
13739 			if(txt.stripInternal.length) {
13740 				drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
13741 			}
13742 
13743 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
13744 				return false;
13745 			} else {
13746 				return true;
13747 			}
13748 		}, Rectangle(smw.position(), bounds.size));
13749 
13750 		/+
13751 		int place = 0;
13752 		int y = 75;
13753 		foreach(width; widths) {
13754 			painter.fillColor = Color.red;
13755 			painter.drawRectangle(Point(place, y), Size(width, 75));
13756 			//y += 15;
13757 			place += width;
13758 		}
13759 		+/
13760 
13761 		return bounds;
13762 	}
13763 
13764 	static class MyTextStyle : TextStyle {
13765 		OperatingSystemFont font_;
13766 		this(OperatingSystemFont font, bool passwordMode = false) {
13767 			this.font_ = font;
13768 		}
13769 
13770 		override OperatingSystemFont font() {
13771 			return font_;
13772 		}
13773 	}
13774 }
13775 
13776 /+
13777 class TextWidget : Widget {
13778 	TextLayouter l;
13779 	ScrollMessageWidget smw;
13780 	TextDisplayHelper helper;
13781 	this(TextLayouter l, Widget parent) {
13782 		this.l = l;
13783 		super(parent);
13784 
13785 		smw = new ScrollMessageWidget(this);
13786 		//smw.horizontalScrollBar.hide;
13787 		//smw.verticalScrollBar.hide;
13788 		smw.addDefaultWheelListeners(16, 16, 8);
13789 		smw.movementPerButtonClick(16, 16);
13790 		helper = new TextDisplayHelper(l, smw);
13791 
13792 		// no need to do this here since there's gonna be a resize
13793 		// event immediately before any drawing
13794 		// smw.setTotalArea(l.width, l.height);
13795 		smw.setViewableArea(
13796 			this.width - this.paddingLeft - this.paddingRight,
13797 			this.height - this.paddingTop - this.paddingBottom);
13798 
13799 		/+
13800 		writeln(l.width, "x", l.height);
13801 		+/
13802 	}
13803 }
13804 +/
13805 
13806 
13807 
13808 
13809 /+
13810 	make sure it calls parentWindow.inputProxy.setIMEPopupLocation too
13811 +/
13812 
13813 /++
13814 	Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit].
13815 +/
13816 abstract class EditableTextWidget : Widget {
13817 	protected this(Widget parent) {
13818 		version(custom_widgets)
13819 			this(true, parent);
13820 		else
13821 			this(false, parent);
13822 	}
13823 
13824 	private bool useCustomWidget;
13825 
13826 	protected this(bool useCustomWidget, Widget parent) {
13827 		this.useCustomWidget = useCustomWidget;
13828 
13829 		super(parent);
13830 
13831 		if(useCustomWidget)
13832 			setupCustomTextEditing();
13833 	}
13834 
13835 	private bool wordWrapEnabled_;
13836 	/++
13837 		Enables or disables wrapping of long lines on word boundaries.
13838 	+/
13839 	void wordWrapEnabled(bool enabled) {
13840 		if(useCustomWidget) {
13841 			wordWrapEnabled_ = enabled;
13842 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
13843 		} else version(win32_widgets) {
13844 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
13845 		}
13846 	}
13847 
13848 	override int minWidth() { return scaleWithDpi(16); }
13849 	override int widthStretchiness() { return 7; }
13850 	override int widthShrinkiness() { return 1; }
13851 
13852 	override int maxHeight() {
13853 		if(useCustomWidget)
13854 			return tdh.maxHeight;
13855 		else
13856 			return super.maxHeight();
13857 	}
13858 
13859 	override void focus() {
13860 		if(useCustomWidget && tdh)
13861 			tdh.focus();
13862 		else
13863 			super.focus();
13864 	}
13865 
13866 	override void defaultEventHandler_focusout(Event foe) {
13867 		if(tdh !is null && foe.target is tdh)
13868 			tdh.redraw();
13869 	}
13870 
13871 	override void defaultEventHandler_focusin(Event foe) {
13872 		if(tdh !is null && foe.target is tdh)
13873 			tdh.redraw();
13874 	}
13875 
13876 
13877 	/++
13878 		Selects all the text in the control, as if the user did it themselves. When the user types in a widget, the selected text is replaced with the new input, so this might be useful for putting in default text that is easy for the user to replace.
13879 	+/
13880 	void selectAll() {
13881 		if(useCustomWidget) {
13882 			tdh.selectAll();
13883 		} else version(win32_widgets) {
13884 			SendMessage(hwnd, EM_SETSEL, 0, -1);
13885 		}
13886 	}
13887 
13888 	/++
13889 		Basic clipboard operations.
13890 
13891 		History:
13892 			Added December 31, 2024
13893 	+/
13894 	void copy() {
13895 		if(useCustomWidget) {
13896 			tdh.copy();
13897 		} else version(win32_widgets) {
13898 			SendMessage(hwnd, WM_COPY, 0, 0);
13899 		}
13900 	}
13901 
13902 	/// ditto
13903 	void cut() {
13904 		if(useCustomWidget) {
13905 			tdh.cut();
13906 		} else version(win32_widgets) {
13907 			SendMessage(hwnd, WM_CUT, 0, 0);
13908 		}
13909 	}
13910 
13911 	/// ditto
13912 	void paste() {
13913 		if(useCustomWidget) {
13914 			tdh.paste();
13915 		} else version(win32_widgets) {
13916 			SendMessage(hwnd, WM_PASTE, 0, 0);
13917 		}
13918 	}
13919 
13920 	///
13921 	void undo() {
13922 		if(useCustomWidget) {
13923 			tdh.undo();
13924 		} else version(win32_widgets) {
13925 			SendMessage(hwnd, EM_UNDO, 0, 0);
13926 		}
13927 	}
13928 
13929 	// note that WM_CLEAR deletes the selection without copying it to the clipboard
13930 	// also windows supports margins, modified flag, and much more
13931 
13932 	// EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here
13933 
13934 	// EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc.
13935 
13936 
13937 
13938 	/*protected*/ TextDisplayHelper tdh;
13939 	/*protected*/ TextLayouter textLayout;
13940 
13941 	/++
13942 		Gets or sets the current content of the control, as a plain text string. Setting the content will reset the cursor position and overwrite any changes the user made.
13943 	+/
13944 	@property string content() {
13945 		if(useCustomWidget) {
13946 			return textLayout.getTextString();
13947 		} else version(win32_widgets) {
13948 			wchar[4096] bufferstack;
13949 			wchar[] buffer;
13950 			auto len = GetWindowTextLength(hwnd);
13951 			if(len < bufferstack.length)
13952 				buffer = bufferstack[0 .. len + 1];
13953 			else
13954 				buffer = new wchar[](len + 1);
13955 
13956 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
13957 			if(l >= 0)
13958 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
13959 			else
13960 				return null;
13961 		}
13962 
13963 		assert(0);
13964 	}
13965 	/// ditto
13966 	@property void content(string s) {
13967 		if(useCustomWidget) {
13968 			with(textLayout.selection) {
13969 				moveToStartOfDocument();
13970 				setAnchor();
13971 				moveToEndOfDocument();
13972 				setFocus();
13973 				replaceContent(s);
13974 			}
13975 
13976 			tdh.adjustScrollbarSizes();
13977 			// these don't seem to help
13978 			// tdh.smw.setPosition(0, 0);
13979 			// tdh.scrollForCaret();
13980 
13981 			redraw();
13982 		} else version(win32_widgets) {
13983 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
13984 			SetWindowTextW(hwnd, bfr.ptr);
13985 		}
13986 	}
13987 
13988 	/++
13989 		Appends some text to the widget at the end, without affecting the user selection or cursor position.
13990 	+/
13991 	void addText(string txt) {
13992 		if(useCustomWidget) {
13993 			textLayout.appendText(txt);
13994 			tdh.adjustScrollbarSizes();
13995 			redraw();
13996 		} else version(win32_widgets) {
13997 			// get the current selection
13998 			DWORD StartPos, EndPos;
13999 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
14000 
14001 			// move the caret to the end of the text
14002 			int outLength = GetWindowTextLengthW(hwnd);
14003 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
14004 
14005 			// insert the text at the new caret position
14006 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
14007 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
14008 
14009 			// restore the previous selection
14010 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
14011 		}
14012 	}
14013 
14014 	// EM_SCROLLCARET scrolls the caret into view
14015 
14016 	void scrollToBottom() {
14017 		if(useCustomWidget) {
14018 			tdh.smw.scrollDown(int.max);
14019 		} else version(win32_widgets) {
14020 			SendMessageW( hwnd, EM_LINESCROLL, 0, int.max );
14021 		}
14022 	}
14023 
14024 	protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14025 		return new TextDisplayHelper(textLayout, smw);
14026 	}
14027 
14028 	protected TextStyle defaultTextStyle() {
14029 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
14030 	}
14031 
14032 	private OperatingSystemFont getUsedFont() {
14033 		auto cs = getComputedStyle();
14034 		auto font = cs.font;
14035 		if(font is null) {
14036 			font = new OperatingSystemFont;
14037 			font.loadDefault();
14038 		}
14039 		return font;
14040 	}
14041 
14042 	protected void setupCustomTextEditing() {
14043 		textLayout = new TextLayouter(defaultTextStyle());
14044 
14045 		auto smw = new ScrollMessageWidget(this);
14046 		if(!showingHorizontalScroll)
14047 			smw.horizontalScrollBar.hide();
14048 		if(!showingVerticalScroll)
14049 			smw.verticalScrollBar.hide();
14050 		this.tabStop = false;
14051 		smw.tabStop = false;
14052 		tdh = textDisplayHelperFactory(textLayout, smw);
14053 	}
14054 
14055 	override void newParentWindow(Window old, Window n) {
14056 		if(n is null) return;
14057 		this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
14058 			if(textLayout) {
14059 				if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
14060 					// the dpi change can change the font, so this informs the layouter that it has changed too
14061 					style.font_ = getUsedFont();
14062 
14063 					// arsd.core.writeln(this.parentWindow.win.actualDpi);
14064 				}
14065 			}
14066 		});
14067 	}
14068 
14069 	static class Style : Widget.Style {
14070 		override WidgetBackground background() {
14071 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
14072 		}
14073 
14074 		override Color foregroundColor() {
14075 			return WidgetPainter.visualTheme.foregroundColor;
14076 		}
14077 
14078 		override FrameStyle borderStyle() {
14079 			return FrameStyle.sunk;
14080 		}
14081 
14082 		override MouseCursor cursor() {
14083 			return GenericCursor.Text;
14084 		}
14085 	}
14086 	mixin OverrideStyle!Style;
14087 
14088 	version(win32_widgets) {
14089 		private string lastContentBlur;
14090 
14091 		override void defaultEventHandler_blur(Event ev) {
14092 			super.defaultEventHandler_blur(ev);
14093 
14094 			if(!useCustomWidget)
14095 			if(this.content != lastContentBlur) {
14096 				auto evt = new ChangeEvent!string(this, &this.content);
14097 				evt.dispatch();
14098 				lastContentBlur = this.content;
14099 			}
14100 		}
14101 	}
14102 
14103 
14104 	bool showingVerticalScroll() { return true; }
14105 	bool showingHorizontalScroll() { return true; }
14106 }
14107 
14108 /++
14109 	A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`.
14110 
14111 	A `CustomLineEdit` always uses the custom implementation, even on operating systems where the native control is implemented in minigui, which may provide more api styling features but at the cost of poorer integration with the OS and potentially worse user experience in other ways.
14112 
14113 	See_Also:
14114 		[PasswordEdit] for a `LineEdit` that obscures its input.
14115 
14116 		[TextEdit] for a multi-line plain text editor widget.
14117 
14118 		[TextLabel] for a single line piece of static text.
14119 
14120 		[TextDisplay] for a read-only display of a larger piece of plain text.
14121 +/
14122 class LineEdit : EditableTextWidget {
14123 	override bool showingVerticalScroll() { return false; }
14124 	override bool showingHorizontalScroll() { return false; }
14125 
14126 	override int flexBasisWidth() { return 250; }
14127 	override int widthShrinkiness() { return 10; }
14128 
14129 	///
14130 	this(Widget parent) {
14131 		super(parent);
14132 		version(win32_widgets) {
14133 			createWin32Window(this, "edit"w, "",
14134 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
14135 		} else version(custom_widgets) {
14136 		} else static assert(false);
14137 	}
14138 
14139 	private this(bool useCustomWidget, Widget parent) {
14140 		if(!useCustomWidget)
14141 			this(parent);
14142 		else
14143 			super(true, parent);
14144 	}
14145 
14146 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14147 		auto tdh = new TextDisplayHelper(textLayout, smw);
14148 		tdh.singleLine = true;
14149 		return tdh;
14150 	}
14151 
14152 	version(win32_widgets) {
14153 		mixin Padding!q{0};
14154 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
14155 		override int maxHeight() { return minHeight; }
14156 	}
14157 
14158 	/+
14159 	@property void passwordMode(bool p) {
14160 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
14161 	}
14162 	+/
14163 }
14164 
14165 /// ditto
14166 class CustomLineEdit : LineEdit {
14167 	this(Widget parent) {
14168 		super(true, parent);
14169 	}
14170 }
14171 
14172 /++
14173 	A [LineEdit] that displays `*` in place of the actual characters.
14174 
14175 	Alas, Windows requires the window to be created differently to use this style,
14176 	so it had to be a new class instead of a toggle on and off on an existing object.
14177 
14178 	History:
14179 		Added January 24, 2021
14180 
14181 		Implemented on Linux on January 31, 2023.
14182 +/
14183 class PasswordEdit : EditableTextWidget {
14184 	override bool showingVerticalScroll() { return false; }
14185 	override bool showingHorizontalScroll() { return false; }
14186 
14187 	override int flexBasisWidth() { return 250; }
14188 
14189 	override TextStyle defaultTextStyle() {
14190 		auto cs = getComputedStyle();
14191 
14192 		auto osf = new class OperatingSystemFont {
14193 			this() {
14194 				super(cs.font);
14195 			}
14196 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
14197 				int count = 0;
14198 				foreach(dchar ch; text)
14199 					count++;
14200 				return count * super.stringWidth("*", window);
14201 			}
14202 		};
14203 
14204 		return new TextDisplayHelper.MyTextStyle(osf);
14205 	}
14206 
14207 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14208 		static class TDH : TextDisplayHelper {
14209 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
14210 				singleLine = true;
14211 				super(textLayout, smw);
14212 			}
14213 
14214 			override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14215 				char[256] buffer = void;
14216 				int bufferLength = 0;
14217 				foreach(dchar ch; text)
14218 					buffer[bufferLength++] = '*';
14219 				painter.drawText(upperLeft, buffer[0..bufferLength]);
14220 			}
14221 		}
14222 
14223 		return new TDH(textLayout, smw);
14224 	}
14225 
14226 	///
14227 	this(Widget parent) {
14228 		super(parent);
14229 		version(win32_widgets) {
14230 			createWin32Window(this, "edit"w, "",
14231 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
14232 		} else version(custom_widgets) {
14233 		} else static assert(false);
14234 	}
14235 
14236 	private this(bool useCustomWidget, Widget parent) {
14237 		if(!useCustomWidget)
14238 			this(parent);
14239 		else
14240 			super(true, parent);
14241 	}
14242 
14243 	version(win32_widgets) {
14244 		mixin Padding!q{2};
14245 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
14246 		override int maxHeight() { return minHeight; }
14247 	}
14248 }
14249 
14250 /// ditto
14251 class CustomPasswordEdit : PasswordEdit {
14252 	this(Widget parent) {
14253 		super(true, parent);
14254 	}
14255 }
14256 
14257 
14258 /++
14259 	A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`.
14260 
14261 	See_Also:
14262 		[TextDisplay] for a read-only text display.
14263 
14264 		[LineEdit] for a single line text editor.
14265 
14266 		[PasswordEdit] for a single line text editor that obscures its input.
14267 +/
14268 class TextEdit : EditableTextWidget {
14269 	///
14270 	this(Widget parent) {
14271 		super(parent);
14272 		version(win32_widgets) {
14273 			createWin32Window(this, "edit"w, "",
14274 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
14275 		} else version(custom_widgets) {
14276 		} else static assert(false);
14277 	}
14278 
14279 	private this(bool useCustomWidget, Widget parent) {
14280 		if(!useCustomWidget)
14281 			this(parent);
14282 		else
14283 			super(true, parent);
14284 	}
14285 
14286 	override int maxHeight() { return int.max; }
14287 	override int heightStretchiness() { return 7; }
14288 
14289 	override int flexBasisWidth() { return 250; }
14290 	override int flexBasisHeight() { return 25; }
14291 }
14292 
14293 /// ditto
14294 class CustomTextEdit : TextEdit {
14295 	this(Widget parent) {
14296 		super(true, parent);
14297 	}
14298 }
14299 
14300 /+
14301 /++
14302 
14303 +/
14304 version(none)
14305 class RichTextDisplay : Widget {
14306 	@property void content(string c) {}
14307 	void appendContent(string c) {}
14308 }
14309 +/
14310 
14311 /++
14312 	A read-only text display. It is based on the editable widget base, but does not allow user edits and displays it on the direct background instead of on an editable background.
14313 
14314 	History:
14315 		Added October 31, 2023 (dub v11.3)
14316 +/
14317 class TextDisplay : EditableTextWidget {
14318 	this(string text, Widget parent) {
14319 		super(true, parent);
14320 		this.content = text;
14321 	}
14322 
14323 	override int maxHeight() { return int.max; }
14324 	override int minHeight() { return Window.defaultLineHeight; }
14325 	override int heightStretchiness() { return 7; }
14326 	override int heightShrinkiness() { return 2; }
14327 
14328 	override int flexBasisWidth() {
14329 		return scaleWithDpi(250);
14330 	}
14331 	override int flexBasisHeight() {
14332 		if(textLayout is null || this.tdh is null)
14333 			return Window.defaultLineHeight;
14334 
14335 		auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height;
14336 		return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height;
14337 	}
14338 
14339 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14340 		return new MyTextDisplayHelper(textLayout, smw);
14341 	}
14342 
14343 	override void registerMovement() {
14344 		super.registerMovement();
14345 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
14346 	}
14347 
14348 	static class MyTextDisplayHelper : TextDisplayHelper {
14349 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
14350 			smw.verticalScrollBar.hide();
14351 			smw.horizontalScrollBar.hide();
14352 			super(textLayout, smw);
14353 			this.readonly = true;
14354 		}
14355 
14356 		override void registerMovement() {
14357 			super.registerMovement();
14358 
14359 			// FIXME: do the horizontal one too as needed and make sure that it does
14360 			// wordwrapping again
14361 			if(l.height + smw.horizontalScrollBar.height > this.height)
14362 				smw.verticalScrollBar.show();
14363 			else
14364 				smw.verticalScrollBar.hide();
14365 
14366 			l.wordWrapWidth = this.width;
14367 
14368 			smw.verticalScrollBar.setPosition = 0;
14369 		}
14370 	}
14371 
14372 	class Style : Widget.Style {
14373 		// just want the generic look for these
14374 	}
14375 
14376 	mixin OverrideStyle!Style;
14377 }
14378 
14379 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
14380 /++
14381 	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.
14382 
14383 
14384 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
14385 
14386 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
14387 
14388 	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.
14389 
14390 	History:
14391 		Added August 12, 2024 (dub v11.6)
14392 +/
14393 abstract class GenericListViewWidget : Widget {
14394 	/++
14395 
14396 	+/
14397 	this(Widget parent) {
14398 		super(parent);
14399 
14400 		smw = new ScrollMessageWidget(this);
14401 		smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width);
14402 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
14403 		smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet
14404 
14405 		inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw));
14406 		inner.tabStop = this.tabStop;
14407 		this.tabStop = false;
14408 	}
14409 
14410 	private ScrollMessageWidget smw;
14411 	private GenericListViewWidgetInner inner;
14412 
14413 	/++
14414 
14415 	+/
14416 	abstract GenericListViewItem itemFactory(Widget parent);
14417 	// in device-dependent pixels
14418 	/++
14419 
14420 	+/
14421 	abstract Size itemSize(); // use 0 to indicate it can stretch?
14422 
14423 	enum LayoutMode {
14424 		rows,
14425 		columns,
14426 		gridRowsFirst,
14427 		gridColumnsFirst
14428 	}
14429 	LayoutMode layoutMode() {
14430 		return LayoutMode.rows;
14431 	}
14432 
14433 	private int itemCount_;
14434 
14435 	/++
14436 		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.
14437 	+/
14438 	void setItemCount(int count) {
14439 		smw.setTotalArea(inner.width, count * itemSize().height);
14440 		smw.setViewableArea(inner.width, inner.height);
14441 		this.itemCount_ = count;
14442 	}
14443 
14444 	/++
14445 		Returns the current count of items expected to available in the list.
14446 	+/
14447 	int itemCount() {
14448 		return this.itemCount_;
14449 	}
14450 
14451 	/++
14452 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
14453 
14454 		Note you must $(I also) call [setItemCount] if the total item count has changed.
14455 	+/
14456 	void notifyItemsChanged(int index, int count = 1) {
14457 	}
14458 	/// ditto
14459 	void notifyItemsInserted(int index, int count = 1) {
14460 	}
14461 	/// ditto
14462 	void notifyItemsRemoved(int index, int count = 1) {
14463 	}
14464 	/// ditto
14465 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
14466 	}
14467 
14468 	/++
14469 		History:
14470 			Added January 1, 2025
14471 	+/
14472 	void ensureItemVisibleInScroll(int index) {
14473 		auto itemPos = index * itemSize().height;
14474 		auto vsb = smw.verticalScrollBar;
14475 		auto viewable = vsb.viewableArea_;
14476 
14477 		if(viewable == 0) {
14478 			// viewable == 0 isn't actually supposed to happen, this means
14479 			// this method is being called before having our size assigned, it should
14480 			// probably just queue it up for later.
14481 			queuedScroll = index;
14482 			return;
14483 		}
14484 
14485 		queuedScroll = int.min;
14486 
14487 		if(itemPos < vsb.position) {
14488 			// scroll up to it
14489 			vsb.setPosition(itemPos);
14490 			smw.notify();
14491 		} else if(itemPos + itemSize().height > (vsb.position + viewable)) {
14492 			// scroll down to it, so it is at the bottom
14493 
14494 			auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height;
14495 			// need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it
14496 
14497 			vsb.setPosition(itemPos - lastViewableItemPosition);
14498 			smw.notify();
14499 		}
14500 	}
14501 
14502 	/++
14503 		History:
14504 			Added January 1, 2025;
14505 	+/
14506 	int numberOfCurrentlyFullyVisibleItems() {
14507 		return smw.verticalScrollBar.viewableArea_ / itemSize.height;
14508 	}
14509 
14510 	private int queuedScroll = int.min;
14511 
14512 	override void recomputeChildLayout() {
14513 		super.recomputeChildLayout();
14514 		if(queuedScroll != int.min)
14515 			ensureItemVisibleInScroll(queuedScroll);
14516 	}
14517 
14518 	private GenericListViewItem[] items;
14519 
14520 	override void paint(WidgetPainter painter) {}
14521 }
14522 
14523 /// ditto
14524 abstract class GenericListViewItem : Widget {
14525 	/++
14526 	+/
14527 	this(Widget parent) {
14528 		super(parent);
14529 	}
14530 
14531 	private int _currentIndex = -1;
14532 
14533 	private void showItemPrivate(int idx) {
14534 		showItem(idx);
14535 		_currentIndex = idx;
14536 	}
14537 
14538 	/++
14539 		Implement this to show an item from your data backing to the list.
14540 
14541 		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.
14542 	+/
14543 	abstract void showItem(int idx);
14544 
14545 	/++
14546 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
14547 
14548 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
14549 
14550 		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.
14551 	+/
14552 	final int currentIndexLoaded() {
14553 		return _currentIndex;
14554 	}
14555 }
14556 
14557 ///
14558 unittest {
14559 	import arsd.minigui;
14560 
14561 	import std.conv;
14562 
14563 	void main() {
14564 		auto mw = new MainWindow();
14565 
14566 		static class MyListViewItem : GenericListViewItem {
14567 			this(Widget parent) {
14568 				super(parent);
14569 
14570 				label = new TextLabel("unloaded", TextAlignment.Left, this);
14571 				button = new Button("Click", this);
14572 
14573 				button.addEventListener("triggered", (){
14574 					messageBox(text("clicked ", currentIndexLoaded()));
14575 				});
14576 			}
14577 			override void showItem(int idx) {
14578 				label.label = "Item " ~ to!string(idx);
14579 			}
14580 
14581 			TextLabel label;
14582 			Button button;
14583 		}
14584 
14585 		auto widget = new class GenericListViewWidget {
14586 			this() {
14587 				super(mw);
14588 			}
14589 			override GenericListViewItem itemFactory(Widget parent) {
14590 				return new MyListViewItem(parent);
14591 			}
14592 			override Size itemSize() {
14593 				return Size(0, scaleWithDpi(80));
14594 			}
14595 		};
14596 
14597 		widget.setItemCount(5000);
14598 
14599 		mw.loop();
14600 	}
14601 }
14602 
14603 // this exists just to wrap the actual GenericListViewWidgetInner so borders
14604 // and padding and stuff can work
14605 private class GenericListViewInnerContainer : Widget {
14606 	this(Widget parent) {
14607 		super(parent);
14608 		this.tabStop = false;
14609 	}
14610 
14611 	override void recomputeChildLayout() {
14612 		registerMovement();
14613 
14614 		auto cs = getComputedStyle();
14615 		auto bw = getBorderWidth(cs.borderStyle);
14616 
14617 		assert(children.length < 2);
14618 		foreach(child; children) {
14619 			child.x = bw + paddingLeft();
14620 			child.y = bw + paddingTop();
14621 			child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight();
14622 			child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom();
14623 
14624 			child.recomputeChildLayout();
14625 		}
14626 	}
14627 
14628 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
14629 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14630 			return parent.parent.parent.useStyleProperties(dg);
14631 		else
14632 			return super.useStyleProperties(dg);
14633 	}
14634 
14635 	override int paddingTop() {
14636 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14637 			return parent.parent.parent.paddingTop();
14638 		else
14639 			return super.paddingTop();
14640 	}
14641 
14642 	override int paddingBottom() {
14643 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14644 			return parent.parent.parent.paddingBottom();
14645 		else
14646 			return super.paddingBottom();
14647 	}
14648 
14649 	override int paddingLeft() {
14650 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14651 			return parent.parent.parent.paddingLeft();
14652 		else
14653 			return super.paddingLeft();
14654 	}
14655 
14656 	override int paddingRight() {
14657 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14658 			return parent.parent.parent.paddingRight();
14659 		else
14660 			return super.paddingRight();
14661 	}
14662 
14663 
14664 }
14665 
14666 private class GenericListViewWidgetInner : Widget {
14667 	this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) {
14668 		super(parent);
14669 		this.glvw = glvw;
14670 
14671 		reloadVisible();
14672 
14673 		smw.addEventListener("scroll", () {
14674 			reloadVisible();
14675 		});
14676 	}
14677 
14678 	override void registerMovement() {
14679 		super.registerMovement();
14680 		if(glvw && glvw.smw)
14681 			glvw.smw.setViewableArea(this.width, this.height);
14682 	}
14683 
14684 	void reloadVisible() {
14685 		auto y = glvw.smw.position.y / glvw.itemSize.height;
14686 
14687 		// idk why i had this here it doesn't seem to be ueful and actually made last items diasppear
14688 		//int offset = glvw.smw.position.y % glvw.itemSize.height;
14689 		//if(offset || y >= glvw.itemCount())
14690 			//y--;
14691 
14692 		if(y < 0)
14693 			y = 0;
14694 
14695 		recomputeChildLayout();
14696 
14697 		foreach(item; glvw.items) {
14698 			if(y < glvw.itemCount()) {
14699 				item.showItemPrivate(y);
14700 				item.show();
14701 			} else {
14702 				item.hide();
14703 			}
14704 			y++;
14705 		}
14706 
14707 		this.redraw();
14708 	}
14709 
14710 	private GenericListViewWidget glvw;
14711 
14712 	private bool inRcl;
14713 	override void recomputeChildLayout() {
14714 		if(inRcl)
14715 			return;
14716 		inRcl = true;
14717 		scope(exit)
14718 			inRcl = false;
14719 
14720 		registerMovement();
14721 
14722 		auto ih = glvw.itemSize().height;
14723 
14724 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
14725 		bool hadNew;
14726 		while(glvw.items.length < itemCount) {
14727 			// FIXME: free the old items? maybe just set length
14728 			glvw.items ~= glvw.itemFactory(this);
14729 			hadNew = true;
14730 		}
14731 
14732 		if(hadNew)
14733 			reloadVisible();
14734 
14735 		int y = -(glvw.smw.position.y % ih) + this.paddingTop();
14736 		foreach(child; children) {
14737 			child.x = this.paddingLeft();
14738 			child.y = y;
14739 			y += glvw.itemSize().height;
14740 			child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight();
14741 			child.height = ih;
14742 
14743 			child.recomputeChildLayout();
14744 		}
14745 	}
14746 }
14747 
14748 
14749 
14750 /++
14751 	History:
14752 		It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`.
14753 +/
14754 class MessageBox : Dialog {
14755 	private string message;
14756 	MessageBoxButton buttonPressed = MessageBoxButton.None;
14757 	/++
14758 
14759 		History:
14760 		The overload that takes `Window originator` was added on September 29, 2024.
14761 	+/
14762 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
14763 		this(null, message, buttons, buttonIds);
14764 	}
14765 	/// ditto
14766 	this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
14767 		message = message.stripRightInternal;
14768 		int mainWidth;
14769 
14770 		// estimate longest line
14771 		int count;
14772 		foreach(ch; message) {
14773 			if(ch == '\n') {
14774 				if(count > mainWidth)
14775 					mainWidth = count;
14776 				count = 0;
14777 			} else {
14778 				count++;
14779 			}
14780 		}
14781 		mainWidth *= 8;
14782 		if(mainWidth < 300)
14783 			mainWidth = 300;
14784 		if(mainWidth > 600)
14785 			mainWidth = 600;
14786 
14787 		super(originator, mainWidth, 100);
14788 
14789 		assert(buttons.length);
14790 		assert(buttons.length ==  buttonIds.length);
14791 
14792 		this.message = message;
14793 
14794 		auto label = new TextDisplay(message, this);
14795 
14796 		auto hl = new HorizontalLayout(this);
14797 		auto spacer = new HorizontalSpacer(hl); // to right align
14798 
14799 		foreach(idx, buttonText; buttons) {
14800 			auto button = new CommandButton(buttonText, hl);
14801 
14802 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
14803 				this.buttonPressed = buttonIds[idx];
14804 				win.close();
14805 			}; })(idx));
14806 
14807 			if(idx == 0)
14808 				button.focus();
14809 		}
14810 
14811 		if(buttons.length == 1)
14812 			auto spacer2 = new HorizontalSpacer(hl); // to center it
14813 
14814 		auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom;
14815 		auto max = scaleWithDpi(600); // random max height
14816 		if(size > max)
14817 			size = max;
14818 
14819 		win.resize(scaleWithDpi(mainWidth), size);
14820 
14821 		win.show();
14822 		redraw();
14823 	}
14824 
14825 	override void OK() {
14826 		this.win.close();
14827 	}
14828 
14829 	mixin Padding!q{16};
14830 }
14831 
14832 ///
14833 enum MessageBoxStyle {
14834 	OK, ///
14835 	OKCancel, ///
14836 	RetryCancel, ///
14837 	YesNo, ///
14838 	YesNoCancel, ///
14839 	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.
14840 }
14841 
14842 ///
14843 enum MessageBoxIcon {
14844 	None, ///
14845 	Info, ///
14846 	Warning, ///
14847 	Error ///
14848 }
14849 
14850 /// Identifies the button the user pressed on a message box.
14851 enum MessageBoxButton {
14852 	None, /// The user closed the message box without clicking any of the buttons.
14853 	OK, ///
14854 	Cancel, ///
14855 	Retry, ///
14856 	Yes, ///
14857 	No, ///
14858 	Continue ///
14859 }
14860 
14861 
14862 /++
14863 	Displays a modal message box, blocking until the user dismisses it. These global ones are discouraged in favor of the same methods on [Window], which give better user experience since the message box is tied the parent window instead of acting independently.
14864 
14865 	Returns: the button pressed.
14866 +/
14867 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
14868 	return messageBox(null, title, message, style, icon);
14869 }
14870 
14871 /// ditto
14872 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
14873 	return messageBox(null, null, message, style, icon);
14874 }
14875 
14876 /++
14877 
14878 +/
14879 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
14880 	version(win32_widgets) {
14881 		WCharzBuffer t = WCharzBuffer(title);
14882 		WCharzBuffer m = WCharzBuffer(message);
14883 		UINT type;
14884 		with(MessageBoxStyle)
14885 		final switch(style) {
14886 			case OK: type |= MB_OK; break;
14887 			case OKCancel: type |= MB_OKCANCEL; break;
14888 			case RetryCancel: type |= MB_RETRYCANCEL; break;
14889 			case YesNo: type |= MB_YESNO; break;
14890 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
14891 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
14892 		}
14893 		with(MessageBoxIcon)
14894 		final switch(icon) {
14895 			case None: break;
14896 			case Info: type |= MB_ICONINFORMATION; break;
14897 			case Warning: type |= MB_ICONWARNING; break;
14898 			case Error: type |= MB_ICONERROR; break;
14899 		}
14900 		switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) {
14901 			case IDOK: return MessageBoxButton.OK;
14902 			case IDCANCEL: return MessageBoxButton.Cancel;
14903 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
14904 			case IDYES: return MessageBoxButton.Yes;
14905 			case IDNO: return MessageBoxButton.No;
14906 			case IDCONTINUE: return MessageBoxButton.Continue;
14907 			default: return MessageBoxButton.None;
14908 		}
14909 	} else {
14910 		string[] buttons;
14911 		MessageBoxButton[] buttonIds;
14912 		with(MessageBoxStyle)
14913 		final switch(style) {
14914 			case OK:
14915 				buttons = ["OK"];
14916 				buttonIds = [MessageBoxButton.OK];
14917 			break;
14918 			case OKCancel:
14919 				buttons = ["OK", "Cancel"];
14920 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
14921 			break;
14922 			case RetryCancel:
14923 				buttons = ["Retry", "Cancel"];
14924 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
14925 			break;
14926 			case YesNo:
14927 				buttons = ["Yes", "No"];
14928 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
14929 			break;
14930 			case YesNoCancel:
14931 				buttons = ["Yes", "No", "Cancel"];
14932 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
14933 			break;
14934 			case RetryCancelContinue:
14935 				buttons = ["Try Again", "Cancel", "Continue"];
14936 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
14937 			break;
14938 		}
14939 		auto mb = new MessageBox(originator, message, buttons, buttonIds);
14940 		EventLoop el = EventLoop.get;
14941 		el.run(() { return !mb.win.closed; });
14942 		return mb.buttonPressed;
14943 	}
14944 
14945 }
14946 
14947 /// ditto
14948 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
14949 	return messageBox(originator, null, message, style, icon);
14950 }
14951 
14952 
14953 ///
14954 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
14955 
14956 /++
14957 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
14958 
14959 	History:
14960 		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.
14961 +/
14962 struct EventListener {
14963 	private Widget widget;
14964 	private string event;
14965 	private EventHandler handler;
14966 	private bool useCapture;
14967 
14968 	///
14969 	void disconnect() {
14970 		widget.removeEventListener(this);
14971 	}
14972 }
14973 
14974 /++
14975 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
14976 
14977 	Now, I recommend you use a statically typed event object instead.
14978 
14979 	See_Also: [Event]
14980 +/
14981 enum EventType : string {
14982 	click = "click", ///
14983 
14984 	mouseenter = "mouseenter", ///
14985 	mouseleave = "mouseleave", ///
14986 	mousein = "mousein", ///
14987 	mouseout = "mouseout", ///
14988 	mouseup = "mouseup", ///
14989 	mousedown = "mousedown", ///
14990 	mousemove = "mousemove", ///
14991 
14992 	keydown = "keydown", ///
14993 	keyup = "keyup", ///
14994 	char_ = "char", ///
14995 
14996 	focus = "focus", ///
14997 	blur = "blur", ///
14998 
14999 	triggered = "triggered", ///
15000 
15001 	change = "change", ///
15002 }
15003 
15004 /++
15005 	Represents an event that is currently being processed.
15006 
15007 
15008 	Minigui's event model is based on the web browser. An event has a name, a target,
15009 	and an associated data object. It starts from the window and works its way down through
15010 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
15011 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
15012 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
15013 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
15014 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
15015 	whenever propagation is done, not only if it gets to the end of the chain).
15016 
15017 	This model has several nice points:
15018 
15019 	$(LIST
15020 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
15021 		  with event handlers set, then add/remove children as much as you want without needing
15022 		  to manage the event handlers on them - the parent alone can manage everything.
15023 
15024 		* It is easy to create new custom events in your application.
15025 
15026 		* It is familiar to many web developers.
15027 	)
15028 
15029 	There's a few downsides though:
15030 
15031 	$(LIST
15032 		* There's not a lot of type safety.
15033 
15034 		* You don't get a static list of what events a widget can emit.
15035 
15036 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
15037 		  the central delegation benefit is it can be lead to debugging of action at a distance.
15038 	)
15039 
15040 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
15041 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
15042 	to simply use a D object type which provides a static interface as well as a built-in event name.
15043 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
15044 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
15045 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
15046 	to having a little more help from the D compiler and documentation generator.
15047 
15048 	Your code would change like this:
15049 
15050 	---
15051 	// old
15052 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
15053 
15054 	// new
15055 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
15056 	---
15057 
15058 	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.
15059 
15060 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
15061 
15062 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
15063 
15064 	Thus the family of functions are:
15065 
15066 	[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.
15067 
15068 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
15069 
15070 	[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.
15071 
15072 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
15073 
15074 	---
15075 	class MyCheckbox : Widget {
15076 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
15077 		/// It is NOT actually required but should be used whenever possible.
15078 		mixin Emits!(ChangeEvent!bool);
15079 
15080 		this(Widget parent) {
15081 			super(parent);
15082 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
15083 		}
15084 
15085 		private bool _checked;
15086 		@property bool checked() { return _checked; }
15087 		@property void checked(bool set) {
15088 			_checked = set;
15089 			emit!(ChangeEvent!bool)(&checked);
15090 		}
15091 	}
15092 	---
15093 
15094 	## Creating Your Own Events
15095 
15096 	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.
15097 
15098 	---
15099 	class MyEvent : Event {
15100 		this(Widget target) { super(EventString, target); }
15101 		mixin Register; // adds EventString and other reflection information
15102 	}
15103 	---
15104 
15105 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
15106 
15107 	History:
15108 		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.
15109 
15110 		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.
15111 +/
15112 /+
15113 
15114 	## General Conventions
15115 
15116 	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.
15117 
15118 
15119 	## Qt-style signals and slots
15120 
15121 	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.
15122 
15123 	The intention is for events to be used when
15124 
15125 	---
15126 	class Demo : Widget {
15127 		this() {
15128 			myPropertyChanged = Signal!int(this);
15129 		}
15130 		@property myProperty(int v) {
15131 			myPropertyChanged.emit(v);
15132 		}
15133 
15134 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
15135 		// but it can just genuinely not care about `this` since that's not really passed.
15136 	}
15137 
15138 	class Foo : Widget {
15139 		// the slot uda is not necessary, but it helps the script and ui builder find it.
15140 		@slot void setValue(int v) { ... }
15141 	}
15142 
15143 	demo.myPropertyChanged.connect(&foo.setValue);
15144 	---
15145 
15146 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
15147 
15148 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
15149 
15150 	class StringChangeEvent : ChangeEvent, Signal!string {
15151 		mixin SignalImpl
15152 	}
15153 
15154 +/
15155 class Event : ReflectableProperties {
15156 	/// Creates an event without populating any members and without sending it. See [dispatch]
15157 	this(string eventName, Widget emittedBy) {
15158 		this.eventName = eventName;
15159 		this.srcElement = emittedBy;
15160 	}
15161 
15162 
15163 	/// Implementations for the [ReflectableProperties] interface/
15164 	void getPropertiesList(scope void delegate(string name) sink) const {}
15165 	/// ditto
15166 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
15167 	/// ditto
15168 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
15169 		return SetPropertyResult.notPermitted;
15170 	}
15171 
15172 
15173 	/+
15174 	/++
15175 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
15176 
15177 		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.
15178 	+/
15179 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
15180 		if(value.length == 0) {
15181 			finalSink(memberName, `""`);
15182 			return;
15183 		}
15184 
15185 		char[1024] bufferBacking;
15186 		char[] buffer = bufferBacking;
15187 		int bufferPosition;
15188 
15189 		void sink(char ch) {
15190 			if(bufferPosition >= buffer.length)
15191 				buffer.length = buffer.length + 1024;
15192 			buffer[bufferPosition++] = ch;
15193 		}
15194 
15195 		sink('"');
15196 
15197 		foreach(ch; value) {
15198 			switch(ch) {
15199 				case '\\':
15200 					sink('\\'); sink('\\');
15201 				break;
15202 				case '"':
15203 					sink('\\'); sink('"');
15204 				break;
15205 				case '\n':
15206 					sink('\\'); sink('n');
15207 				break;
15208 				case '\r':
15209 					sink('\\'); sink('r');
15210 				break;
15211 				case '\t':
15212 					sink('\\'); sink('t');
15213 				break;
15214 				default:
15215 					sink(ch);
15216 			}
15217 		}
15218 
15219 		sink('"');
15220 
15221 		finalSink(memberName, buffer[0 .. bufferPosition]);
15222 	}
15223 	+/
15224 
15225 	/+
15226 	enum EventInitiator {
15227 		system,
15228 		minigui,
15229 		user
15230 	}
15231 
15232 	immutable EventInitiator; initiatedBy;
15233 	+/
15234 
15235 	/++
15236 		Events should generally follow the propagation model, but there's some exceptions
15237 		to that rule. If so, they should override this to return false. In that case, only
15238 		bubbling event handlers on the target itself and capturing event handlers on the containing
15239 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
15240 		capture -> target -> bubble process.)
15241 
15242 		History:
15243 			Added May 12, 2021
15244 	+/
15245 	bool propagates() const pure nothrow @nogc @safe {
15246 		return true;
15247 	}
15248 
15249 	/++
15250 		hints as to whether preventDefault will actually do anything. not entirely reliable.
15251 
15252 		History:
15253 			Added May 14, 2021
15254 	+/
15255 	bool cancelable() const pure nothrow @nogc @safe {
15256 		return true;
15257 	}
15258 
15259 	/++
15260 		You can mix this into child class to register some boilerplate. It includes the `EventString`
15261 		member, a constructor, and implementations of the dynamic get data interfaces.
15262 
15263 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
15264 
15265 
15266 		You can override the default EventString by simply providing your own in the form of
15267 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
15268 		which provides some namespace protection against conflicts in other libraries while still being fairly
15269 		easy to use.
15270 
15271 		If you provide your own constructor, it will override the default constructor provided here. A constructor
15272 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
15273 		first argument to your constructor.
15274 
15275 		History:
15276 			Added May 13, 2021.
15277 	+/
15278 	protected static mixin template Register() {
15279 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
15280 		this(Widget target) { super(EventString, target); }
15281 
15282 		mixin ReflectableProperties.RegisterGetters;
15283 	}
15284 
15285 	/++
15286 		This is the widget that emitted the event.
15287 
15288 
15289 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
15290 
15291 		History:
15292 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
15293 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
15294 			so I don't intend to remove these aliases.
15295 	+/
15296 	Widget source;
15297 	/// ditto
15298 	alias source target;
15299 	/// ditto
15300 	alias source srcElement;
15301 
15302 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
15303 
15304 	/// Prevents the default event handler (if there is one) from being called
15305 	void preventDefault() {
15306 		lastDefaultPrevented = true;
15307 		defaultPrevented = true;
15308 	}
15309 
15310 	/// Stops the event propagation immediately.
15311 	void stopPropagation() {
15312 		propagationStopped = true;
15313 	}
15314 
15315 	private bool defaultPrevented;
15316 	private bool propagationStopped;
15317 	private string eventName;
15318 
15319 	private bool isBubbling;
15320 
15321 	/// 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.
15322 	protected void adjustScrolling() { }
15323 	/// ditto
15324 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
15325 
15326 	/++
15327 		this sends it only to the target. If you want propagation, use dispatch() instead.
15328 
15329 		This should be made private!!!
15330 
15331 	+/
15332 	void sendDirectly() {
15333 		if(srcElement is null)
15334 			return;
15335 
15336 		// 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.
15337 
15338 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
15339 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
15340 
15341 		adjustScrolling();
15342 
15343 		if(auto e = target.parentWindow) {
15344 			if(auto handlers = "*" in e.capturingEventHandlers)
15345 			foreach(handler; *handlers)
15346 				if(handler) handler(e, this);
15347 			if(auto handlers = eventName in e.capturingEventHandlers)
15348 			foreach(handler; *handlers)
15349 				if(handler) handler(e, this);
15350 		}
15351 
15352 		auto e = srcElement;
15353 
15354 		if(auto handlers = eventName in e.bubblingEventHandlers)
15355 		foreach(handler; *handlers)
15356 			if(handler) handler(e, this);
15357 
15358 		if(auto handlers = "*" in e.bubblingEventHandlers)
15359 		foreach(handler; *handlers)
15360 			if(handler) handler(e, this);
15361 
15362 		// there's never a default for a catch-all event
15363 		if(!defaultPrevented)
15364 			if(eventName in e.defaultEventHandlers)
15365 				e.defaultEventHandlers[eventName](e, this);
15366 	}
15367 
15368 	/// this dispatches the element using the capture -> target -> bubble process
15369 	void dispatch() {
15370 		if(srcElement is null)
15371 			return;
15372 
15373 		if(!propagates) {
15374 			sendDirectly;
15375 			return;
15376 		}
15377 
15378 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
15379 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
15380 
15381 		adjustScrolling();
15382 		// first capture, then bubble
15383 
15384 		Widget[] chain;
15385 		Widget curr = srcElement;
15386 		while(curr) {
15387 			auto l = curr;
15388 			chain ~= l;
15389 			curr = curr.parent;
15390 		}
15391 
15392 		isBubbling = false;
15393 
15394 		foreach_reverse(e; chain) {
15395 			if(auto handlers = "*" in e.capturingEventHandlers)
15396 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15397 
15398 			if(propagationStopped)
15399 				break;
15400 
15401 			if(auto handlers = eventName in e.capturingEventHandlers)
15402 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15403 
15404 			// the default on capture should really be to always do nothing
15405 
15406 			//if(!defaultPrevented)
15407 			//	if(eventName in e.defaultEventHandlers)
15408 			//		e.defaultEventHandlers[eventName](e.element, this);
15409 
15410 			if(propagationStopped)
15411 				break;
15412 		}
15413 
15414 		int adjustX;
15415 		int adjustY;
15416 
15417 		isBubbling = true;
15418 		if(!propagationStopped)
15419 		foreach(e; chain) {
15420 			if(auto handlers = eventName in e.bubblingEventHandlers)
15421 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15422 
15423 			if(propagationStopped)
15424 				break;
15425 
15426 			if(auto handlers = "*" in e.bubblingEventHandlers)
15427 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15428 
15429 			if(propagationStopped)
15430 				break;
15431 
15432 			if(e.encapsulatedChildren()) {
15433 				adjustClientCoordinates(adjustX, adjustY);
15434 				target = e;
15435 			} else {
15436 				adjustX += e.x;
15437 				adjustY += e.y;
15438 			}
15439 		}
15440 
15441 		if(!defaultPrevented)
15442 		foreach(e; chain) {
15443 			if(eventName in e.defaultEventHandlers)
15444 				e.defaultEventHandlers[eventName](e, this);
15445 		}
15446 	}
15447 
15448 
15449 	/* old compatibility things */
15450 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
15451 	final @property {
15452 		Key key() { return (cast(KeyEventBase) this).key; }
15453 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
15454 
15455 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
15456 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
15457 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
15458 	}
15459 
15460 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
15461 	final @property {
15462 		int clientX() { return (cast(MouseEventBase) this).clientX; }
15463 		int clientY() { return (cast(MouseEventBase) this).clientY; }
15464 
15465 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
15466 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
15467 
15468 		int button() { return (cast(MouseEventBase) this).button; }
15469 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
15470 	}
15471 
15472 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
15473 	final @property {
15474 		int state() {
15475 			if(auto meb = cast(MouseEventBase) this)
15476 				return meb.state;
15477 			if(auto keb = cast(KeyEventBase) this)
15478 				return keb.state;
15479 			assert(0);
15480 		}
15481 	}
15482 
15483 	deprecated("Use a CharEvent instead of Event in your handler going forward")
15484 	final @property {
15485 		dchar character() {
15486 			if(auto ce = cast(CharEvent) this)
15487 				return ce.character;
15488 			return dchar.init;
15489 		}
15490 	}
15491 
15492 	// for change events
15493 	@property {
15494 		///
15495 		int intValue() { return 0; }
15496 		///
15497 		string stringValue() { return null; }
15498 	}
15499 }
15500 
15501 /++
15502 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
15503 
15504 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
15505 	dynamic and custom events, but the static list helps ensure you get them right.
15506 
15507 	If this is declared, you can use [Widget.emit] to send the event.
15508 
15509 	All events work the same way though, following the capture->widget->bubble model described under [Event].
15510 
15511 	History:
15512 		Added May 4, 2021
15513 +/
15514 mixin template Emits(EventType) {
15515 	import arsd.minigui : EventString;
15516 	static if(is(EventType : Event) && !is(EventType == Event))
15517 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
15518 	else
15519 		static assert(0, "You can only emit subclasses of Event");
15520 }
15521 
15522 /// ditto
15523 mixin template Emits(string eventString) {
15524 	mixin("private Event[0] emits_" ~ eventString ~";");
15525 }
15526 
15527 /*
15528 class SignalEvent(string name) : Event {
15529 
15530 }
15531 */
15532 
15533 /++
15534 	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".
15535 
15536 
15537 	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.
15538 
15539 	History:
15540 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
15541 +/
15542 class CommandEvent : Event {
15543 	enum EventString = "command";
15544 	this(Widget source, string CommandString = EventString) {
15545 		super(CommandString, source);
15546 	}
15547 }
15548 
15549 /++
15550 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
15551 +/
15552 class CommandEventWithArgs(Args...) : CommandEvent {
15553 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
15554 	Args args;
15555 }
15556 
15557 /++
15558 	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.
15559 
15560 	See [CommandEvent] for more information.
15561 
15562 	Returns:
15563 		The [EventListener] you can use to remove the handler.
15564 +/
15565 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
15566 	return w.addEventListener(CommandString, (Event ev) {
15567 		if(ev.target is w)
15568 			return; // it does not consume its own commands!
15569 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
15570 			handler(cev.args);
15571 			ev.stopPropagation();
15572 		}
15573 	});
15574 }
15575 
15576 /++
15577 	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.
15578 +/
15579 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
15580 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
15581 	event.dispatch();
15582 }
15583 
15584 /++
15585 
15586 +/
15587 class ResizeEvent : Event {
15588 	enum EventString = "resize";
15589 
15590 	this(Widget target) { super(EventString, target); }
15591 
15592 	override bool propagates() const { return false; }
15593 }
15594 
15595 /++
15596 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
15597 
15598 	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.
15599 
15600 	History:
15601 		Added June 21, 2021 (dub v10.1)
15602 +/
15603 class ClosingEvent : Event {
15604 	enum EventString = "closing";
15605 
15606 	this(Widget target) { super(EventString, target); }
15607 
15608 	override bool propagates() const { return false; }
15609 	override bool cancelable() const { return true; }
15610 }
15611 
15612 /// ditto
15613 class ClosedEvent : Event {
15614 	enum EventString = "closed";
15615 
15616 	this(Widget target) { super(EventString, target); }
15617 
15618 	override bool propagates() const { return false; }
15619 	override bool cancelable() const { return false; }
15620 }
15621 
15622 ///
15623 class BlurEvent : Event {
15624 	enum EventString = "blur";
15625 
15626 	// FIXME: related target?
15627 	this(Widget target) { super(EventString, target); }
15628 
15629 	override bool propagates() const { return false; }
15630 }
15631 
15632 ///
15633 class FocusEvent : Event {
15634 	enum EventString = "focus";
15635 
15636 	// FIXME: related target?
15637 	this(Widget target) { super(EventString, target); }
15638 
15639 	override bool propagates() const { return false; }
15640 }
15641 
15642 /++
15643 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
15644 
15645 	History:
15646 		Added July 3, 2021
15647 +/
15648 class FocusInEvent : Event {
15649 	enum EventString = "focusin";
15650 
15651 	// FIXME: related target?
15652 	this(Widget target) { super(EventString, target); }
15653 
15654 	override bool cancelable() const { return false; }
15655 }
15656 
15657 /// ditto
15658 class FocusOutEvent : Event {
15659 	enum EventString = "focusout";
15660 
15661 	// FIXME: related target?
15662 	this(Widget target) { super(EventString, target); }
15663 
15664 	override bool cancelable() const { return false; }
15665 }
15666 
15667 ///
15668 class ScrollEvent : Event {
15669 	enum EventString = "scroll";
15670 	this(Widget target) { super(EventString, target); }
15671 
15672 	override bool cancelable() const { return false; }
15673 }
15674 
15675 /++
15676 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
15677 
15678 	History:
15679 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
15680 +/
15681 class CharEvent : Event {
15682 	enum EventString = "char";
15683 	this(Widget target, dchar ch) {
15684 		character = ch;
15685 		super(EventString, target);
15686 	}
15687 
15688 	immutable dchar character;
15689 }
15690 
15691 /++
15692 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
15693 +/
15694 abstract class ChangeEventBase : Event {
15695 	enum EventString = "change";
15696 	this(Widget target) {
15697 		super(EventString, target);
15698 	}
15699 
15700 	/+
15701 		// idk where or how exactly i want to do this.
15702 		// i might come back to it later.
15703 
15704 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
15705 	// this way the source doesn't get too confused (think of a nested scroll widget)
15706 	//
15707 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
15708 	// then you consume that command and change you scroll x position to whatever. then you do
15709 	// some kind of change event that is broadcast back to the children and any horizontal scroll
15710 	// listeners are now able to update, without having an explicit connection between them.
15711 	void broadcastToChildren(string fieldName) {
15712 
15713 	}
15714 	+/
15715 }
15716 
15717 /++
15718 	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.
15719 
15720 
15721 	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).
15722 
15723 	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);`
15724 
15725 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
15726 
15727 	History:
15728 		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.
15729 +/
15730 class ChangeEvent(T) : ChangeEventBase {
15731 	this(Widget target, T delegate() getNewValue) {
15732 		assert(getNewValue !is null);
15733 		this.getNewValue = getNewValue;
15734 		super(target);
15735 	}
15736 
15737 	private T delegate() getNewValue;
15738 
15739 	/++
15740 		Gets the new value that just changed.
15741 	+/
15742 	@property T value() {
15743 		return getNewValue();
15744 	}
15745 
15746 	/// compatibility method for old generic Events
15747 	static if(is(immutable T == immutable int))
15748 		override int intValue() { return value; }
15749 	/// ditto
15750 	static if(is(immutable T == immutable string))
15751 		override string stringValue() { return value; }
15752 }
15753 
15754 /++
15755 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
15756 
15757 
15758 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
15759 
15760 	History:
15761 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
15762 +/
15763 abstract class KeyEventBase : Event {
15764 	this(string name, Widget target) {
15765 		super(name, target);
15766 	}
15767 
15768 	// for key events
15769 	Key key; ///
15770 
15771 	KeyEvent originalKeyEvent;
15772 
15773 	/++
15774 		Indicates the current state of the given keyboard modifier keys.
15775 
15776 		History:
15777 			Added to events on April 15, 2020.
15778 	+/
15779 	bool ctrlKey;
15780 
15781 	/// ditto
15782 	bool altKey;
15783 
15784 	/// ditto
15785 	bool shiftKey;
15786 
15787 	/++
15788 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
15789 
15790 		See [arsd.simpledisplay.ModifierState] for other possible flags.
15791 	+/
15792 	int state;
15793 
15794 	mixin Register;
15795 }
15796 
15797 /++
15798 	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].
15799 
15800 
15801 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
15802 
15803 	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.
15804 
15805 	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.
15806 
15807 	See_Also: [KeyUpEvent], [CharEvent]
15808 
15809 	History:
15810 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
15811 +/
15812 class KeyDownEvent : KeyEventBase {
15813 	enum EventString = "keydown";
15814 	this(Widget target) { super(EventString, target); }
15815 }
15816 
15817 /++
15818 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
15819 
15820 
15821 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
15822 
15823 	See_Also: [KeyDownEvent], [CharEvent]
15824 
15825 	History:
15826 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
15827 +/
15828 class KeyUpEvent : KeyEventBase {
15829 	enum EventString = "keyup";
15830 	this(Widget target) { super(EventString, target); }
15831 }
15832 
15833 /++
15834 	Contains shared properties for various mouse events;
15835 
15836 
15837 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
15838 
15839 	History:
15840 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
15841 +/
15842 abstract class MouseEventBase : Event {
15843 	this(string name, Widget target) {
15844 		super(name, target);
15845 	}
15846 
15847 	// for mouse events
15848 	int clientX; /// The mouse event location relative to the target widget
15849 	int clientY; /// ditto
15850 
15851 	int viewportX; /// The mouse event location relative to the window origin
15852 	int viewportY; /// ditto
15853 
15854 	int button; /// See: [MouseEvent.button]
15855 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
15856 
15857 	/++
15858 		Indicates the current state of the given keyboard modifier keys.
15859 
15860 		History:
15861 			Added to mouse events on September 28, 2010.
15862 	+/
15863 	bool ctrlKey;
15864 
15865 	/// ditto
15866 	bool altKey;
15867 
15868 	/// ditto
15869 	bool shiftKey;
15870 
15871 
15872 
15873 	int state; ///
15874 
15875 	/++
15876 		for consistent names with key event.
15877 
15878 		History:
15879 			Added September 28, 2021 (dub v10.3)
15880 	+/
15881 	alias modifierState = state;
15882 
15883 	/++
15884 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
15885 
15886 		History:
15887 			Added May 15, 2021
15888 	+/
15889 	bool isMouseWheel() {
15890 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
15891 	}
15892 
15893 	// private
15894 	override void adjustClientCoordinates(int deltaX, int deltaY) {
15895 		clientX += deltaX;
15896 		clientY += deltaY;
15897 	}
15898 
15899 	override void adjustScrolling() {
15900 	version(custom_widgets) { // TEMP
15901 		viewportX = clientX;
15902 		viewportY = clientY;
15903 		if(auto se = cast(ScrollableWidget) srcElement) {
15904 			clientX += se.scrollOrigin.x;
15905 			clientY += se.scrollOrigin.y;
15906 		} else if(auto se = cast(ScrollableContainerWidget) srcElement) {
15907 			//clientX += se.scrollX_;
15908 			//clientY += se.scrollY_;
15909 		}
15910 	}
15911 	}
15912 
15913 	mixin Register;
15914 }
15915 
15916 /++
15917 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
15918 
15919 
15920 	$(WARNING
15921 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
15922 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
15923 		behavior.
15924 	)
15925 
15926 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
15927 
15928 	[MouseUpEvent] is sent when the user releases a mouse button.
15929 
15930 	[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.)
15931 
15932 	[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.
15933 
15934 	[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.
15935 
15936 	[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.
15937 
15938 	[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.
15939 
15940 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
15941 
15942 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
15943 
15944 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
15945 
15946 	Rationale:
15947 
15948 		If you only want to do drag, mousedown/up works just fine being consistently sent.
15949 
15950 		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).
15951 
15952 		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.
15953 
15954 	History:
15955 		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.
15956 +/
15957 class MouseUpEvent : MouseEventBase {
15958 	enum EventString = "mouseup"; ///
15959 	this(Widget target) { super(EventString, target); }
15960 }
15961 /// ditto
15962 class MouseDownEvent : MouseEventBase {
15963 	enum EventString = "mousedown"; ///
15964 	this(Widget target) { super(EventString, target); }
15965 }
15966 /// ditto
15967 class MouseMoveEvent : MouseEventBase {
15968 	enum EventString = "mousemove"; ///
15969 	this(Widget target) { super(EventString, target); }
15970 }
15971 /// ditto
15972 class ClickEvent : MouseEventBase {
15973 	enum EventString = "click"; ///
15974 	this(Widget target) { super(EventString, target); }
15975 }
15976 /// ditto
15977 class DoubleClickEvent : MouseEventBase {
15978 	enum EventString = "dblclick"; ///
15979 	this(Widget target) { super(EventString, target); }
15980 }
15981 /// ditto
15982 class MouseOverEvent : Event {
15983 	enum EventString = "mouseover"; ///
15984 	this(Widget target) { super(EventString, target); }
15985 }
15986 /// ditto
15987 class MouseOutEvent : Event {
15988 	enum EventString = "mouseout"; ///
15989 	this(Widget target) { super(EventString, target); }
15990 }
15991 /// ditto
15992 class MouseEnterEvent : Event {
15993 	enum EventString = "mouseenter"; ///
15994 	this(Widget target) { super(EventString, target); }
15995 
15996 	override bool propagates() const { return false; }
15997 }
15998 /// ditto
15999 class MouseLeaveEvent : Event {
16000 	enum EventString = "mouseleave"; ///
16001 	this(Widget target) { super(EventString, target); }
16002 
16003 	override bool propagates() const { return false; }
16004 }
16005 
16006 private bool isAParentOf(Widget a, Widget b) {
16007 	if(a is null || b is null)
16008 		return false;
16009 
16010 	while(b !is null) {
16011 		if(a is b)
16012 			return true;
16013 		b = b.parent;
16014 	}
16015 
16016 	return false;
16017 }
16018 
16019 private struct WidgetAtPointResponse {
16020 	Widget widget;
16021 
16022 	// x, y relative to the widget in the response.
16023 	int x;
16024 	int y;
16025 }
16026 
16027 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
16028 	assert(starting !is null);
16029 
16030 	starting.addScrollPosition(x, y);
16031 
16032 	auto child = starting.getChildAtPosition(x, y);
16033 	while(child) {
16034 		if(child.hidden)
16035 			continue;
16036 		starting = child;
16037 		x -= child.x;
16038 		y -= child.y;
16039 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
16040 		child = r.widget;
16041 		if(child is starting)
16042 			break;
16043 	}
16044 	return WidgetAtPointResponse(starting, x, y);
16045 }
16046 
16047 version(win32_widgets) {
16048 private:
16049 	import core.sys.windows.commctrl;
16050 
16051 	pragma(lib, "comctl32");
16052 	shared static this() {
16053 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
16054 		INITCOMMONCONTROLSEX ic;
16055 		ic.dwSize = cast(DWORD) ic.sizeof;
16056 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
16057 		if(!InitCommonControlsEx(&ic)) {
16058 			//writeln("ICC failed");
16059 		}
16060 	}
16061 
16062 
16063 	// everything from here is just win32 headers copy pasta
16064 private:
16065 extern(Windows):
16066 
16067 	alias HANDLE HMENU;
16068 	HMENU CreateMenu();
16069 	bool SetMenu(HWND, HMENU);
16070 	HMENU CreatePopupMenu();
16071 	enum MF_POPUP = 0x10;
16072 	enum MF_STRING = 0;
16073 
16074 
16075 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
16076 	struct INITCOMMONCONTROLSEX {
16077 		DWORD dwSize;
16078 		DWORD dwICC;
16079 	}
16080 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
16081 enum {
16082         IDB_STD_SMALL_COLOR,
16083         IDB_STD_LARGE_COLOR,
16084         IDB_VIEW_SMALL_COLOR = 4,
16085         IDB_VIEW_LARGE_COLOR = 5
16086 }
16087 enum {
16088         STD_CUT,
16089         STD_COPY,
16090         STD_PASTE,
16091         STD_UNDO,
16092         STD_REDOW,
16093         STD_DELETE,
16094         STD_FILENEW,
16095         STD_FILEOPEN,
16096         STD_FILESAVE,
16097         STD_PRINTPRE,
16098         STD_PROPERTIES,
16099         STD_HELP,
16100         STD_FIND,
16101         STD_REPLACE,
16102         STD_PRINT // = 14
16103 }
16104 
16105 alias HANDLE HIMAGELIST;
16106 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
16107 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
16108         BOOL ImageList_Destroy(HIMAGELIST);
16109 
16110 uint MAKELONG(ushort a, ushort b) {
16111         return cast(uint) ((b << 16) | a);
16112 }
16113 
16114 
16115 struct TBBUTTON {
16116 	int   iBitmap;
16117 	int   idCommand;
16118 	BYTE  fsState;
16119 	BYTE  fsStyle;
16120 	version(Win64)
16121 	BYTE[6] bReserved;
16122 	else
16123 	BYTE[2]  bReserved;
16124 	DWORD dwData;
16125 	INT_PTR   iString;
16126 }
16127 
16128 	enum {
16129 		TB_ADDBUTTONSA   = WM_USER + 20,
16130 		TB_INSERTBUTTONA = WM_USER + 21,
16131 		TB_GETIDEALSIZE = WM_USER + 99,
16132 	}
16133 
16134 struct SIZE {
16135 	LONG cx;
16136 	LONG cy;
16137 }
16138 
16139 
16140 enum {
16141 	TBSTATE_CHECKED       = 1,
16142 	TBSTATE_PRESSED       = 2,
16143 	TBSTATE_ENABLED       = 4,
16144 	TBSTATE_HIDDEN        = 8,
16145 	TBSTATE_INDETERMINATE = 16,
16146 	TBSTATE_WRAP          = 32
16147 }
16148 
16149 
16150 
16151 enum {
16152 	ILC_COLOR    = 0,
16153 	ILC_COLOR4   = 4,
16154 	ILC_COLOR8   = 8,
16155 	ILC_COLOR16  = 16,
16156 	ILC_COLOR24  = 24,
16157 	ILC_COLOR32  = 32,
16158 	ILC_COLORDDB = 254,
16159 	ILC_MASK     = 1,
16160 	ILC_PALETTE  = 2048
16161 }
16162 
16163 
16164 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
16165 
16166 
16167 enum {
16168 	TB_ENABLEBUTTON          = WM_USER + 1,
16169 	TB_CHECKBUTTON,
16170 	TB_PRESSBUTTON,
16171 	TB_HIDEBUTTON,
16172 	TB_INDETERMINATE, //     = WM_USER + 5,
16173 	TB_ISBUTTONENABLED       = WM_USER + 9,
16174 	TB_ISBUTTONCHECKED,
16175 	TB_ISBUTTONPRESSED,
16176 	TB_ISBUTTONHIDDEN,
16177 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
16178 	TB_SETSTATE              = WM_USER + 17,
16179 	TB_GETSTATE              = WM_USER + 18,
16180 	TB_ADDBITMAP             = WM_USER + 19,
16181 	TB_DELETEBUTTON          = WM_USER + 22,
16182 	TB_GETBUTTON,
16183 	TB_BUTTONCOUNT,
16184 	TB_COMMANDTOINDEX,
16185 	TB_SAVERESTOREA,
16186 	TB_CUSTOMIZE,
16187 	TB_ADDSTRINGA,
16188 	TB_GETITEMRECT,
16189 	TB_BUTTONSTRUCTSIZE,
16190 	TB_SETBUTTONSIZE,
16191 	TB_SETBITMAPSIZE,
16192 	TB_AUTOSIZE, //          = WM_USER + 33,
16193 	TB_GETTOOLTIPS           = WM_USER + 35,
16194 	TB_SETTOOLTIPS           = WM_USER + 36,
16195 	TB_SETPARENT             = WM_USER + 37,
16196 	TB_SETROWS               = WM_USER + 39,
16197 	TB_GETROWS,
16198 	TB_GETBITMAPFLAGS,
16199 	TB_SETCMDID,
16200 	TB_CHANGEBITMAP,
16201 	TB_GETBITMAP,
16202 	TB_GETBUTTONTEXTA,
16203 	TB_REPLACEBITMAP, //     = WM_USER + 46,
16204 	TB_GETBUTTONSIZE         = WM_USER + 58,
16205 	TB_SETBUTTONWIDTH        = WM_USER + 59,
16206 	TB_GETBUTTONTEXTW        = WM_USER + 75,
16207 	TB_SAVERESTOREW          = WM_USER + 76,
16208 	TB_ADDSTRINGW            = WM_USER + 77,
16209 }
16210 
16211 extern(Windows)
16212 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
16213 
16214 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
16215 
16216 
16217 	enum {
16218 		TB_SETINDENT = WM_USER + 47,
16219 		TB_SETIMAGELIST,
16220 		TB_GETIMAGELIST,
16221 		TB_LOADIMAGES,
16222 		TB_GETRECT,
16223 		TB_SETHOTIMAGELIST,
16224 		TB_GETHOTIMAGELIST,
16225 		TB_SETDISABLEDIMAGELIST,
16226 		TB_GETDISABLEDIMAGELIST,
16227 		TB_SETSTYLE,
16228 		TB_GETSTYLE,
16229 		//TB_GETBUTTONSIZE,
16230 		//TB_SETBUTTONWIDTH,
16231 		TB_SETMAXTEXTROWS,
16232 		TB_GETTEXTROWS // = WM_USER + 61
16233 	}
16234 
16235 enum {
16236 	CCM_FIRST            = 0x2000,
16237 	CCM_LAST             = CCM_FIRST + 0x200,
16238 	CCM_SETBKCOLOR       = 8193,
16239 	CCM_SETCOLORSCHEME   = 8194,
16240 	CCM_GETCOLORSCHEME   = 8195,
16241 	CCM_GETDROPTARGET    = 8196,
16242 	CCM_SETUNICODEFORMAT = 8197,
16243 	CCM_GETUNICODEFORMAT = 8198,
16244 	CCM_SETVERSION       = 0x2007,
16245 	CCM_GETVERSION       = 0x2008,
16246 	CCM_SETNOTIFYWINDOW  = 0x2009
16247 }
16248 
16249 
16250 enum {
16251 	PBM_SETRANGE     = WM_USER + 1,
16252 	PBM_SETPOS,
16253 	PBM_DELTAPOS,
16254 	PBM_SETSTEP,
16255 	PBM_STEPIT,   // = WM_USER + 5
16256 	PBM_SETRANGE32   = 1030,
16257 	PBM_GETRANGE,
16258 	PBM_GETPOS,
16259 	PBM_SETBARCOLOR, // = 1033
16260 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
16261 }
16262 
16263 enum {
16264 	PBS_SMOOTH   = 1,
16265 	PBS_VERTICAL = 4
16266 }
16267 
16268 enum {
16269         ICC_LISTVIEW_CLASSES = 1,
16270         ICC_TREEVIEW_CLASSES = 2,
16271         ICC_BAR_CLASSES      = 4,
16272         ICC_TAB_CLASSES      = 8,
16273         ICC_UPDOWN_CLASS     = 16,
16274         ICC_PROGRESS_CLASS   = 32,
16275         ICC_HOTKEY_CLASS     = 64,
16276         ICC_ANIMATE_CLASS    = 128,
16277         ICC_WIN95_CLASSES    = 255,
16278         ICC_DATE_CLASSES     = 256,
16279         ICC_USEREX_CLASSES   = 512,
16280         ICC_COOL_CLASSES     = 1024,
16281 	ICC_STANDARD_CLASSES = 0x00004000,
16282 }
16283 
16284 	enum WM_USER = 1024;
16285 }
16286 
16287 version(win32_widgets)
16288 	pragma(lib, "comdlg32");
16289 
16290 
16291 ///
16292 enum GenericIcons : ushort {
16293 	None, ///
16294 	// these happen to match the win32 std icons numerically if you just subtract one from the value
16295 	Cut, ///
16296 	Copy, ///
16297 	Paste, ///
16298 	Undo, ///
16299 	Redo, ///
16300 	Delete, ///
16301 	New, ///
16302 	Open, ///
16303 	Save, ///
16304 	PrintPreview, ///
16305 	Properties, ///
16306 	Help, ///
16307 	Find, ///
16308 	Replace, ///
16309 	Print, ///
16310 }
16311 
16312 enum FileDialogType {
16313 	Automatic,
16314 	Open,
16315 	Save
16316 }
16317 
16318 /++
16319 	The default string [FileName] refers to to store the last file referenced. You can use this if you like, or provide a different variable to `FileName` in your function.
16320 +/
16321 string previousFileReferenced;
16322 
16323 /++
16324 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
16325 
16326 	Params:
16327 		storage = an alias to a `static string` variable that stores the last file referenced. It will
16328 		use this to pre-fill the dialog with a suggestion.
16329 
16330 		Please note that it MUST be `static` or you will get compile errors.
16331 
16332 		filters = the filters param to [getFileName]
16333 
16334 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
16335 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
16336 		a save dialog box. Otherwise, it will show an open dialog box.
16337 +/
16338 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
16339 	string name;
16340 	alias name this;
16341 
16342 	@implicit this(string name) {
16343 		this.name = name;
16344 	}
16345 }
16346 
16347 /++
16348 	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.
16349 
16350 	History:
16351 		onCancel was added November 6, 2021.
16352 
16353 		The dialog itself on Linux was modified on December 2, 2021 to include
16354 		a directory picker in addition to the command line completion view.
16355 
16356 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
16357 
16358 		The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version.
16359 	Future_directions:
16360 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
16361 		at least on Linux, maybe on Windows too.
16362 +/
16363 void getOpenFileName(
16364 	Window owner,
16365 	void delegate(string) onOK,
16366 	string prefilledName = null,
16367 	string[] filters = null,
16368 	void delegate() onCancel = null,
16369 	string initialDirectory = null,
16370 )
16371 {
16372 	return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory);
16373 }
16374 
16375 /// ditto
16376 void getSaveFileName(
16377 	Window owner,
16378 	void delegate(string) onOK,
16379 	string prefilledName = null,
16380 	string[] filters = null,
16381 	void delegate() onCancel = null,
16382 	string initialDirectory = null,
16383 )
16384 {
16385 	return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory);
16386 }
16387 
16388 // 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.")
16389 /// ditto
16390 void getOpenFileName(
16391 	void delegate(string) onOK,
16392 	string prefilledName = null,
16393 	string[] filters = null,
16394 	void delegate() onCancel = null,
16395 	string initialDirectory = null,
16396 )
16397 {
16398 	return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory);
16399 }
16400 
16401 /// ditto
16402 void getSaveFileName(
16403 	void delegate(string) onOK,
16404 	string prefilledName = null,
16405 	string[] filters = null,
16406 	void delegate() onCancel = null,
16407 	string initialDirectory = null,
16408 )
16409 {
16410 	return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory);
16411 }
16412 
16413 /++
16414 	It is possible to override or customize the file dialog in some cases. These members provide those hooks: you do `fileDialogDelegate = new YourSubclassOf_FileDialogDelegate;` and you can do your own thing.
16415 
16416 	This is a customization hook and you should not call methods on this class directly. Use the public functions [getOpenFileName] and [getSaveFileName], or make an automatic dialog with [FileName] instead.
16417 
16418 	History:
16419 		Added January 1, 2025
16420 +/
16421 class FileDialogDelegate {
16422 
16423 	/++
16424 
16425 	+/
16426 	static abstract class PreviewWidget : Widget {
16427 		/// Call this from your subclass' constructor
16428 		this(Widget parent) {
16429 			super(parent);
16430 		}
16431 
16432 		/// Load the file given to you and show its preview inside the widget here
16433 		abstract void previewFile(string filename);
16434 	}
16435 
16436 	/++
16437 		Override this to add preview capabilities to the dialog for certain files.
16438 	+/
16439 	protected PreviewWidget makePreviewWidget(Widget parent) {
16440 		return null;
16441 	}
16442 
16443 	/++
16444 		Override this to change the dialog entirely.
16445 
16446 		This function IS allowed to block, but is NOT required to.
16447 	+/
16448 	protected void getFileName(
16449 		Window owner,
16450 		bool openOrSave, // true if open, false if save
16451 		void delegate(string) onOK,
16452 		string prefilledName,
16453 		string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
16454 		void delegate() onCancel,
16455 		string initialDirectory,
16456 	)
16457 	{
16458 
16459 		version(win32_widgets) {
16460 			import core.sys.windows.commdlg;
16461 		/*
16462 		Ofn.lStructSize = sizeof(OPENFILENAME);
16463 		Ofn.hwndOwner = hWnd;
16464 		Ofn.lpstrFilter = szFilter;
16465 		Ofn.lpstrFile= szFile;
16466 		Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
16467 		Ofn.lpstrFileTitle = szFileTitle;
16468 		Ofn.nMaxFileTitle = sizeof(szFileTitle);
16469 		Ofn.lpstrInitialDir = (LPSTR)NULL;
16470 		Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
16471 		Ofn.lpstrTitle = szTitle;
16472 		 */
16473 
16474 
16475 			wchar[1024] file = 0;
16476 			wchar[1024] filterBuffer = 0;
16477 			makeWindowsString(prefilledName, file[]);
16478 			OPENFILENAME ofn;
16479 			ofn.lStructSize = ofn.sizeof;
16480 			ofn.hwndOwner = owner is null ? null : owner.win.hwnd;
16481 			if(filters.length) {
16482 				string filter;
16483 				foreach(i, f; filters) {
16484 					filter ~= f;
16485 					filter ~= "\0";
16486 				}
16487 				filter ~= "\0";
16488 				ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
16489 			}
16490 			ofn.lpstrFile = file.ptr;
16491 			ofn.nMaxFile = file.length;
16492 
16493 			wchar[1024] initialDir = 0;
16494 			if(initialDirectory !is null) {
16495 				makeWindowsString(initialDirectory, initialDir[]);
16496 				ofn.lpstrInitialDir = file.ptr;
16497 			}
16498 
16499 			if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
16500 			{
16501 				string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
16502 				if(okString.length && okString[$-1] == '\0')
16503 					okString = okString[0..$-1];
16504 				onOK(okString);
16505 			} else {
16506 				if(onCancel)
16507 					onCancel();
16508 			}
16509 		} else version(custom_widgets) {
16510 			filters ~= ["All Files\0*.*"];
16511 			auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner);
16512 			picker.onOK = onOK;
16513 			picker.onCancel = onCancel;
16514 			picker.show();
16515 		}
16516 	}
16517 
16518 }
16519 
16520 /// ditto
16521 FileDialogDelegate fileDialogDelegate() {
16522 	if(fileDialogDelegate_ is null)
16523 		fileDialogDelegate_ = new FileDialogDelegate();
16524 	return fileDialogDelegate_;
16525 }
16526 
16527 /// ditto
16528 void fileDialogDelegate(FileDialogDelegate replacement) {
16529 	fileDialogDelegate_ = replacement;
16530 }
16531 
16532 private FileDialogDelegate fileDialogDelegate_;
16533 
16534 struct FileNameFilter {
16535 	string description;
16536 	string[] globPatterns;
16537 
16538 	string toString() {
16539 		string ret;
16540 		ret ~= description;
16541 		ret ~= " (";
16542 		foreach(idx, pattern; globPatterns) {
16543 			if(idx)
16544 				ret ~= "; ";
16545 			ret ~= pattern;
16546 		}
16547 		ret ~= ")";
16548 
16549 		return ret;
16550 	}
16551 
16552 	static FileNameFilter fromString(string s) {
16553 		size_t end = s.length;
16554 		size_t start = 0;
16555 		foreach_reverse(idx, ch; s) {
16556 			if(ch == ')' && end == s.length)
16557 				end = idx;
16558 			else if(ch == '(' && end != s.length) {
16559 				start = idx + 1;
16560 				break;
16561 			}
16562 		}
16563 
16564 		FileNameFilter fnf;
16565 		fnf.description = s[0 .. start ? start - 1 : 0];
16566 		size_t globStart = 0;
16567 		s = s[start .. end];
16568 		foreach(idx, ch; s)
16569 			if(ch == ';') {
16570 				auto ptn = stripInternal(s[globStart .. idx]);
16571 				if(ptn.length)
16572 					fnf.globPatterns ~= ptn;
16573 				globStart = idx + 1;
16574 
16575 			}
16576 		auto ptn = stripInternal(s[globStart .. $]);
16577 		if(ptn.length)
16578 			fnf.globPatterns ~= ptn;
16579 		return fnf;
16580 	}
16581 }
16582 
16583 struct FileNameFilterSet {
16584 	FileNameFilter[] filters;
16585 
16586 	static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) {
16587 		FileNameFilter[] ret;
16588 
16589 		foreach(filter; filters) {
16590 			FileNameFilter fnf;
16591 			size_t filterStartPoint;
16592 			foreach(idx, ch; filter) {
16593 				if(ch == 0) {
16594 					fnf.description = filter[0 .. idx];
16595 					filterStartPoint = idx + 1;
16596 				} else if(filterStartPoint && ch == ';') {
16597 					fnf.globPatterns ~= filter[filterStartPoint .. idx];
16598 					filterStartPoint = idx + 1;
16599 				}
16600 			}
16601 			fnf.globPatterns ~= filter[filterStartPoint .. $];
16602 
16603 			ret ~= fnf;
16604 		}
16605 
16606 		return FileNameFilterSet(ret);
16607 	}
16608 }
16609 
16610 void getFileName(
16611 	Window owner,
16612 	bool openOrSave,
16613 	void delegate(string) onOK,
16614 	string prefilledName = null,
16615 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
16616 	void delegate() onCancel = null,
16617 	string initialDirectory = null,
16618 )
16619 {
16620 	return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory);
16621 }
16622 
16623 version(custom_widgets)
16624 private
16625 class FilePicker : Dialog {
16626 	void delegate(string) onOK;
16627 	void delegate() onCancel;
16628 	LabeledLineEdit lineEdit;
16629 	bool isOpenDialogInsteadOfSave;
16630 
16631 	static struct HistoryItem {
16632 		string cwd;
16633 		FileNameFilter filters;
16634 	}
16635 	HistoryItem[] historyStack;
16636 	size_t historyStackPosition;
16637 
16638 	void back() {
16639 		if(historyStackPosition) {
16640 			historyStackPosition--;
16641 			currentDirectory = historyStack[historyStackPosition].cwd;
16642 			currentFilter = historyStack[historyStackPosition].filters;
16643 			filesOfType.content = currentFilter.toString();
16644 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
16645 			lineEdit.focus();
16646 		}
16647 	}
16648 
16649 	void forward() {
16650 		if(historyStackPosition + 1 < historyStack.length) {
16651 			historyStackPosition++;
16652 			currentDirectory = historyStack[historyStackPosition].cwd;
16653 			currentFilter = historyStack[historyStackPosition].filters;
16654 			filesOfType.content = currentFilter.toString();
16655 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
16656 			lineEdit.focus();
16657 		}
16658 	}
16659 
16660 	void up() {
16661 		currentDirectory = currentDirectory ~ "..";
16662 		loadFiles(currentDirectory, currentFilter);
16663 		lineEdit.focus();
16664 	}
16665 
16666 	void refresh() {
16667 		loadFiles(currentDirectory, currentFilter);
16668 		lineEdit.focus();
16669 	}
16670 
16671 	// returns common prefix
16672 	static struct CommonPrefixInfo {
16673 		string commonPrefix;
16674 		int fileCount;
16675 		string exactMatch;
16676 	}
16677 	CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) {
16678 
16679 		if(!comingFromHistory) {
16680 			if(historyStack.length) {
16681 				historyStack = historyStack[0 .. historyStackPosition + 1];
16682 				historyStack.assumeSafeAppend();
16683 			}
16684 			historyStack ~= HistoryItem(cwd, filters);
16685 			historyStackPosition = historyStack.length - 1;
16686 		}
16687 
16688 		string[] files;
16689 		string[] dirs;
16690 
16691 		dirs ~= "$HOME";
16692 		dirs ~= "$PWD";
16693 
16694 		string commonPrefix;
16695 		int commonPrefixCount;
16696 		string exactMatch;
16697 
16698 		bool matchesFilter(string name) {
16699 			foreach(filter; filters.globPatterns) {
16700 			if(
16701 				filter.length <= 1 ||
16702 				filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .*
16703 				(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
16704 				(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
16705 			)
16706 			{
16707 				if(name.length > 1 && name[0] == '.')
16708 					if(filter.length == 0 || filter[0] != '.')
16709 						return false;
16710 
16711 				return true;
16712 			}
16713 			}
16714 
16715 			return false;
16716 		}
16717 
16718 		void considerCommonPrefix(string name, bool prefiltered) {
16719 			if(!prefiltered && !matchesFilter(name))
16720 				return;
16721 
16722 			if(commonPrefix is null) {
16723 				commonPrefix = name;
16724 				commonPrefixCount = 1;
16725 				exactMatch = commonPrefix;
16726 			} else {
16727 				foreach(idx, char i; name) {
16728 					if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
16729 						commonPrefix = commonPrefix[0 .. idx];
16730 						commonPrefixCount ++;
16731 						exactMatch = null;
16732 						break;
16733 					}
16734 				}
16735 			}
16736 		}
16737 
16738 		bool applyFilterToDirectories = true;
16739 		bool showDotFiles = false;
16740 		foreach(filter; filters.globPatterns) {
16741 			if(filter == ".*")
16742 				showDotFiles = true;
16743 			else foreach(ch; filter)
16744 				if(ch == '.') {
16745 					// a filter like *.exe should not apply to the directory
16746 					applyFilterToDirectories = false;
16747 					break;
16748 				}
16749 		}
16750 
16751 		try
16752 		getFiles(cwd, (string name, bool isDirectory) {
16753 			if(name == ".")
16754 				return; // skip this as unnecessary
16755 			if(isDirectory) {
16756 				if(applyFilterToDirectories) {
16757 					if(matchesFilter(name)) {
16758 						dirs ~= name;
16759 						considerCommonPrefix(name, false);
16760 					}
16761 				} else if(name != ".." && name.length > 1 && name[0] == '.') {
16762 					if(showDotFiles) {
16763 						dirs ~= name;
16764 						considerCommonPrefix(name, false);
16765 					}
16766 				} else {
16767 					dirs ~= name;
16768 					considerCommonPrefix(name, false);
16769 				}
16770 			} else {
16771 				if(matchesFilter(name)) {
16772 					files ~= name;
16773 
16774 					//if(filter.length > 0 && filter[$-1] == '*') {
16775 						considerCommonPrefix(name, true);
16776 					//}
16777 				}
16778 			}
16779 		});
16780 		catch(ArsdExceptionBase e) {
16781 			messageBox("Unable to read requested directory");
16782 			// FIXME: give them a chance to create it? or at least go back?
16783 			/+
16784 			comingFromHistory = true;
16785 			back();
16786 			return null;
16787 			+/
16788 		}
16789 
16790 		extern(C) static int comparator(scope const void* a, scope const void* b) {
16791 			// FIXME: make it a natural sort for numbers
16792 			// maybe put dot files at the end too.
16793 			auto sa = *cast(string*) a;
16794 			auto sb = *cast(string*) b;
16795 
16796 			for(int i = 0; i < sa.length; i++) {
16797 				if(i == sb.length)
16798 					return 1;
16799 				auto diff = sa[i] - sb[i];
16800 				if(diff)
16801 					return diff;
16802 			}
16803 
16804 			return 0;
16805 		}
16806 
16807 		nonPhobosSort(files, &comparator);
16808 		nonPhobosSort(dirs, &comparator);
16809 
16810 		listWidget.clear();
16811 		dirWidget.clear();
16812 		foreach(name; dirs)
16813 			dirWidget.addOption(name);
16814 		foreach(name; files)
16815 			listWidget.addOption(name);
16816 
16817 		return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch);
16818 	}
16819 
16820 	ListWidget listWidget;
16821 	ListWidget dirWidget;
16822 
16823 	FreeEntrySelection filesOfType;
16824 	LineEdit directoryHolder;
16825 
16826 	string currentDirectory_;
16827 	FileNameFilter currentNonTabFilter;
16828 	FileNameFilter currentFilter;
16829 	FileNameFilterSet filterOptions;
16830 
16831 	void currentDirectory(string s) {
16832 		currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString();
16833 		directoryHolder.content = currentDirectory_;
16834 	}
16835 	string currentDirectory() {
16836 		return currentDirectory_;
16837 	}
16838 
16839 	private string getUserHomeDir() {
16840 		import core.stdc.stdlib;
16841 		version(Windows)
16842 			return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup;
16843 		else
16844 			return (stringz(getenv("HOME")).borrow).idup;
16845 	}
16846 
16847 	private string expandTilde(string s) {
16848 		// FIXME: cannot look up other user dirs
16849 		if(s.length == 1 && s == "~")
16850 			return getUserHomeDir();
16851 		if(s.length > 1 && s[0] == '~' && s[1] == '/')
16852 			return getUserHomeDir() ~ s[1 .. $];
16853 		return s;
16854 	}
16855 
16856 	// FIXME: allow many files to be picked too sometimes
16857 
16858 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
16859 	this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) {
16860 		this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat);
16861 		this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave;
16862 		super(owner, 500, 400, "Choose File..."); // owner);
16863 
16864 		{
16865 			auto navbar = new HorizontalLayout(24, this);
16866 			auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar);
16867 			auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar);
16868 			auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button
16869 
16870 			directoryHolder = new LineEdit(navbar);
16871 
16872 			directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) {
16873 				if(kde.key == Key.Enter || kde.key == Key.PadEnter) {
16874 					kde.stopPropagation();
16875 
16876 					currentDirectory = directoryHolder.content;
16877 					loadFiles(currentDirectory, currentFilter);
16878 
16879 					lineEdit.focus();
16880 				}
16881 			});
16882 
16883 			auto refreshButton = new ToolButton(new Action("R", 0, &this.refresh), navbar); // can live without refresh since you can cancel and reopen but still nice. it should be automatic when it can maybe.
16884 
16885 			/+
16886 			auto newDirectoryButton = new ToolButton(new Action("N"), navbar);
16887 
16888 			// FIXME: make sure putting `.` in the dir filter goes back to the CWD
16889 			// and that ~ goes back to the home dir
16890 			// and blanking it goes back to the suggested dir
16891 
16892 			auto homeButton = new ToolButton(new Action("H"), navbar);
16893 			auto cwdButton = new ToolButton(new Action("."), navbar);
16894 			auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar);
16895 			+/
16896 
16897 			filesOfType = new class FreeEntrySelection {
16898 				this() {
16899 					string[] opt;
16900 					foreach(option; filterOptions.filters)
16901 						opt ~=  option.toString;
16902 					super(opt, navbar);
16903 				}
16904 				override int flexBasisWidth() {
16905 					return scaleWithDpi(150);
16906 				}
16907 				override int widthStretchiness() {
16908 					return 1;//super.widthStretchiness() / 2;
16909 				}
16910 			};
16911 			filesOfType.setSelection(0);
16912 			currentFilter = filterOptions.filters[0];
16913 			currentNonTabFilter = currentFilter;
16914 		}
16915 
16916 		{
16917 			auto mainGrid = new GridLayout(4, 1, this);
16918 
16919 			dirWidget = new ListWidget(mainGrid);
16920 			listWidget = new ListWidget(mainGrid);
16921 			listWidget.tabStop = false;
16922 			dirWidget.tabStop = false;
16923 
16924 			FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid);
16925 
16926 			mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1);
16927 			mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1);
16928 			if(previewWidget)
16929 				mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1);
16930 
16931 			// double click events normally trigger something else but
16932 			// here user might be clicking kinda fast and we'd rather just
16933 			// keep it
16934 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
16935 				auto ce = new ChangeEvent!void(dirWidget, () {});
16936 				ce.dispatch();
16937 				lineEdit.focus();
16938 			});
16939 
16940 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
16941 				string v;
16942 				foreach(o; dirWidget.options)
16943 					if(o.selected) {
16944 						v = o.label;
16945 						break;
16946 					}
16947 				if(v.length) {
16948 					if(v == "$HOME")
16949 						currentDirectory = getUserHomeDir();
16950 					else if(v == "$PWD")
16951 						currentDirectory = ".";
16952 					else
16953 						currentDirectory = currentDirectory ~ "/" ~ v;
16954 					loadFiles(currentDirectory, currentFilter);
16955 				}
16956 
16957 				dirWidget.focusOn = -1;
16958 				lineEdit.focus();
16959 			});
16960 
16961 			// double click here, on the other hand, selects the file
16962 			// and moves on
16963 			listWidget.addEventListener((scope DoubleClickEvent dev) {
16964 				OK();
16965 			});
16966 		}
16967 
16968 		lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this);
16969 		lineEdit.focus();
16970 		lineEdit.addEventListener(delegate(CharEvent event) {
16971 			if(event.character == '\t' || event.character == '\n')
16972 				event.preventDefault();
16973 		});
16974 
16975 		listWidget.addEventListener(EventType.change, () {
16976 			foreach(o; listWidget.options)
16977 				if(o.selected)
16978 					lineEdit.content = o.label;
16979 		});
16980 
16981 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
16982 		loadFiles(currentDirectory, currentFilter);
16983 
16984 		filesOfType.addEventListener(delegate (ChangeEvent!string ce) {
16985 			currentFilter = FileNameFilter.fromString(ce.stringValue);
16986 			currentNonTabFilter = currentFilter;
16987 			loadFiles(currentDirectory, currentFilter);
16988 			// lineEdit.focus(); // this causes a recursive crash.....
16989 		});
16990 
16991 		lineEdit.addEventListener((KeyDownEvent event) {
16992 			if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) {
16993 
16994 				auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory));
16995 				currentDirectory = path.directoryName;
16996 				auto current = path.filename;
16997 
16998 				auto newFilter = current;
16999 				if(current.length && current[0] != '*' && current[$-1] != '*')
17000 					newFilter ~= "*";
17001 				else if(newFilter.length == 0)
17002 					newFilter = "*";
17003 
17004 				auto newFilterObj = FileNameFilter("Custom filter", [newFilter]);
17005 
17006 				CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj);
17007 				if(commonPrefix.fileCount == 1) {
17008 					// exactly one file, let's see what it is
17009 					auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory));
17010 					if(getFileType(specificFile.toString) == FileType.dir) {
17011 						// a directory means we should change to it and keep the old filter
17012 						currentDirectory = specificFile.toString();
17013 						lineEdit.content = specificFile.toString() ~ "/";
17014 						loadFiles(currentDirectory, currentFilter);
17015 					} else {
17016 						// any other file should be selected in the list
17017 						currentDirectory = specificFile.directoryName;
17018 						current = specificFile.filename;
17019 						lineEdit.content = current;
17020 						loadFiles(currentDirectory, currentFilter);
17021 					}
17022 				} else if(commonPrefix.fileCount > 1) {
17023 					currentFilter = newFilterObj;
17024 					filesOfType.content = currentFilter.toString();
17025 					lineEdit.content = commonPrefix.commonPrefix;
17026 				} else {
17027 					// if there were no files, we don't really want to change the filter..
17028 					sdpyPrintDebugString("no files");
17029 				}
17030 
17031 				// FIXME: if that is a directory, add the slash? or even go inside?
17032 
17033 				event.preventDefault();
17034 			}
17035 		});
17036 
17037 		lineEdit.content = expandTilde(prefilledName);
17038 
17039 		auto hl = new HorizontalLayout(60, this);
17040 		auto cancelButton = new Button("Cancel", hl);
17041 		auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl);
17042 
17043 		cancelButton.addEventListener(EventType.triggered, &Cancel);
17044 		okButton.addEventListener(EventType.triggered, &OK);
17045 
17046 		this.addEventListener((KeyDownEvent event) {
17047 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
17048 				event.preventDefault();
17049 				OK();
17050 			}
17051 			else if(event.key == Key.Escape)
17052 				Cancel();
17053 			else if(event.key == Key.F5)
17054 				refresh();
17055 			else if(event.key == Key.Up && event.altKey)
17056 				up(); // ditto
17057 			else if(event.key == Key.Left && event.altKey)
17058 				back(); // FIXME: it sends the key to the line edit too
17059 			else if(event.key == Key.Right && event.altKey)
17060 				forward(); // ditto
17061 			else if(event.key == Key.Up)
17062 				listWidget.setSelection(listWidget.getSelection() - 1);
17063 			else if(event.key == Key.Down)
17064 				listWidget.setSelection(listWidget.getSelection() + 1);
17065 		});
17066 
17067 		// FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted
17068 		// FIXME: button to create new directory
17069 		// FIXME: show dirs in the files list too? idk.
17070 
17071 		// FIXME: support ~ as alias for home in the input
17072 		// FIXME: tab complete ought to be able to change+complete dir too
17073 	}
17074 
17075 	override void OK() {
17076 		if(lineEdit.content.length) {
17077 			auto c = expandTilde(lineEdit.content);
17078 
17079 			FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory));
17080 
17081 			auto ft = getFileType(accepted.toString);
17082 
17083 			if(ft == FileType.error && isOpenDialogInsteadOfSave) {
17084 				// FIXME: tell the user why
17085 				messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel.");
17086 				lineEdit.focus();
17087 				return;
17088 
17089 			}
17090 
17091 			// FIXME: symlinks to dirs should prolly also get this behavior
17092 			if(ft == FileType.dir) {
17093 				currentDirectory = accepted.toString;
17094 
17095 				currentFilter = currentNonTabFilter;
17096 				filesOfType.content = currentFilter.toString();
17097 
17098 				loadFiles(currentDirectory, currentFilter);
17099 				lineEdit.content = "";
17100 
17101 				lineEdit.focus();
17102 
17103 				return;
17104 			}
17105 
17106 			if(onOK)
17107 				onOK(accepted.toString);
17108 		}
17109 		close();
17110 	}
17111 
17112 	override void Cancel() {
17113 		if(onCancel)
17114 			onCancel();
17115 		close();
17116 	}
17117 }
17118 
17119 private enum FileType {
17120 	error,
17121 	dir,
17122 	other
17123 }
17124 
17125 private FileType getFileType(string name) {
17126 	version(Windows) {
17127 		auto ws = WCharzBuffer(name);
17128 		auto ret = GetFileAttributesW(ws.ptr);
17129 		if(ret == INVALID_FILE_ATTRIBUTES)
17130 			return FileType.error;
17131 		return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other;
17132 	} else version(Posix) {
17133 		import core.sys.posix.sys.stat;
17134 		stat_t buf;
17135 		auto ret = stat((name ~ '\0').ptr, &buf);
17136 		if(ret == -1)
17137 			return FileType.error;
17138 		return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
17139 	} else assert(0, "Not implemented");
17140 }
17141 
17142 /*
17143 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
17144 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
17145 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
17146 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
17147 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
17148 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
17149 http://www.sbin.org/doc/Xlib/chapt_03.html
17150 
17151 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
17152 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
17153 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
17154 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
17155 */
17156 
17157 
17158 // These are all for setMenuAndToolbarFromAnnotatedCode
17159 /// This item in the menu will be preceded by a separator line
17160 /// Group: generating_from_code
17161 struct separator {}
17162 deprecated("It was misspelled, use separator instead") alias seperator = separator;
17163 /// Program-wide keyboard shortcut to trigger the action
17164 /// Group: generating_from_code
17165 struct accelerator { string keyString; }
17166 /// tells which menu the action will be on
17167 /// Group: generating_from_code
17168 struct menu { string name; }
17169 /// Describes which toolbar section the action appears on
17170 /// Group: generating_from_code
17171 struct toolbar { string groupName; }
17172 ///
17173 /// Group: generating_from_code
17174 struct icon { ushort id; }
17175 ///
17176 /// Group: generating_from_code
17177 struct label { string label; }
17178 ///
17179 /// Group: generating_from_code
17180 struct hotkey { dchar ch; }
17181 ///
17182 /// Group: generating_from_code
17183 struct tip { string tip; }
17184 ///
17185 /// Group: generating_from_code
17186 enum context_menu = menu.init;
17187 
17188 
17189 /++
17190 	Observes and allows inspection of an object via automatic gui
17191 +/
17192 /// Group: generating_from_code
17193 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
17194 	return new ObjectInspectionWindowImpl!(T)(t);
17195 }
17196 
17197 class ObjectInspectionWindow : Window {
17198 	this(int a, int b, string c) {
17199 		super(a, b, c);
17200 	}
17201 
17202 	abstract void readUpdatesFromObject();
17203 }
17204 
17205 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
17206 	T t;
17207 	this(T t) {
17208 		this.t = t;
17209 
17210 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
17211 
17212 		foreach(memberName; __traits(derivedMembers, T)) {{
17213 			alias member = I!(__traits(getMember, t, memberName))[0];
17214 			alias type = typeof(member);
17215 			static if(is(type == int)) {
17216 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
17217 				//le.addEventListener("char", (Event ev) {
17218 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
17219 						//ev.preventDefault();
17220 				//});
17221 				le.addEventListener(EventType.change, (Event ev) {
17222 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
17223 				});
17224 
17225 				updateMemberDelegates[memberName] = () {
17226 					le.content = toInternal!string(__traits(getMember, t, memberName));
17227 				};
17228 			}
17229 		}}
17230 	}
17231 
17232 	void delegate()[string] updateMemberDelegates;
17233 
17234 	override void readUpdatesFromObject() {
17235 		foreach(k, v; updateMemberDelegates)
17236 			v();
17237 	}
17238 }
17239 
17240 /++
17241 	Creates a dialog based on a data structure.
17242 
17243 	---
17244 	dialog(window, (YourStructure value) {
17245 		// the user filled in the struct and clicked OK,
17246 		// you can check the members now
17247 	});
17248 	---
17249 
17250 	Params:
17251 		initialData = the initial value to show in the dialog. It will not modify this unless
17252 		it is a class then it might, no promises.
17253 
17254 	History:
17255 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
17256 
17257 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
17258 		be deprecated soon.
17259 +/
17260 /// Group: generating_from_code
17261 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17262 	dialog(null, T.init, onOK, onCancel, title);
17263 }
17264 /// ditto
17265 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17266 	dialog(null, T.init, onOK, onCancel, title);
17267 }
17268 /// ditto
17269 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17270 	dialog(parent, T.init, onOK, onCancel, title);
17271 }
17272 /// ditto
17273 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17274 	dialog(parent, initialData, onOK, onCancel, title);
17275 }
17276 /// ditto
17277 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17278 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
17279 	dg.show();
17280 }
17281 
17282 private static template I(T...) { alias I = T; }
17283 
17284 
17285 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
17286 	if(name == "id")
17287 		return allLowerCase ? name : "ID";
17288 
17289 	char[160] buffer;
17290 	int bufferIndex = 0;
17291 	bool shouldCap = true;
17292 	bool shouldSpace;
17293 	bool lastWasCap;
17294 	foreach(idx, char ch; name) {
17295 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
17296 
17297 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
17298 			if(lastWasCap) {
17299 				// two caps in a row, don't change. Prolly acronym.
17300 			} else {
17301 				if(idx)
17302 					shouldSpace = true; // new word, add space
17303 			}
17304 
17305 			lastWasCap = true;
17306 		} else {
17307 			lastWasCap = false;
17308 		}
17309 
17310 		if(shouldSpace) {
17311 			buffer[bufferIndex++] = space;
17312 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
17313 			shouldSpace = false;
17314 		}
17315 		if(shouldCap) {
17316 			if(ch >= 'a' && ch <= 'z')
17317 				ch -= 32;
17318 			shouldCap = false;
17319 		}
17320 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
17321 			ch += 32;
17322 		buffer[bufferIndex++] = ch;
17323 	}
17324 	return buffer[0 .. bufferIndex].idup;
17325 }
17326 
17327 /++
17328 	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.
17329 +/
17330 class AutomaticDialog(T) : Dialog {
17331 	T t;
17332 
17333 	void delegate(T) onOK;
17334 	void delegate() onCancel;
17335 
17336 	override int paddingTop() { return defaultLineHeight; }
17337 	override int paddingBottom() { return defaultLineHeight; }
17338 	override int paddingRight() { return defaultLineHeight; }
17339 	override int paddingLeft() { return defaultLineHeight; }
17340 
17341 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
17342 		assert(onOK !is null);
17343 
17344 		t = initialData;
17345 
17346 		static if(is(T == class)) {
17347 			if(t is null)
17348 				t = new T();
17349 		}
17350 		this.onOK = onOK;
17351 		this.onCancel = onCancel;
17352 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
17353 
17354 		static if(is(T == class))
17355 			this.addDataControllerWidget(t);
17356 		else
17357 			this.addDataControllerWidget(&t);
17358 
17359 		auto hl = new HorizontalLayout(this);
17360 		auto stretch = new HorizontalSpacer(hl); // to right align
17361 		auto ok = new CommandButton("OK", hl);
17362 		auto cancel = new CommandButton("Cancel", hl);
17363 		ok.addEventListener(EventType.triggered, &OK);
17364 		cancel.addEventListener(EventType.triggered, &Cancel);
17365 
17366 		this.addEventListener((KeyDownEvent ev) {
17367 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
17368 				ok.focus();
17369 				OK();
17370 				ev.preventDefault();
17371 			}
17372 			if(ev.key == Key.Escape) {
17373 				Cancel();
17374 				ev.preventDefault();
17375 			}
17376 		});
17377 
17378 		this.addEventListener((scope ClosedEvent ce) {
17379 			if(onCancel)
17380 				onCancel();
17381 		});
17382 
17383 		//this.children[0].focus();
17384 	}
17385 
17386 	override void OK() {
17387 		onOK(t);
17388 		close();
17389 	}
17390 
17391 	override void Cancel() {
17392 		if(onCancel)
17393 			onCancel();
17394 		close();
17395 	}
17396 }
17397 
17398 private template baseClassCount(Class) {
17399 	private int helper() {
17400 		int count = 0;
17401 		static if(is(Class bases == super)) {
17402 			foreach(base; bases)
17403 				static if(is(base == class))
17404 					count += 1 + baseClassCount!base;
17405 		}
17406 		return count;
17407 	}
17408 
17409 	enum int baseClassCount = helper();
17410 }
17411 
17412 private long stringToLong(string s) {
17413 	long ret;
17414 	if(s.length == 0)
17415 		return ret;
17416 	bool negative = s[0] == '-';
17417 	if(negative)
17418 		s = s[1 .. $];
17419 	foreach(ch; s) {
17420 		if(ch >= '0' && ch <= '9') {
17421 			ret *= 10;
17422 			ret += ch - '0';
17423 		}
17424 	}
17425 	if(negative)
17426 		ret = -ret;
17427 	return ret;
17428 }
17429 
17430 
17431 interface ReflectableProperties {
17432 	/++
17433 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
17434 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
17435 		json in the current implementation.
17436 
17437 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
17438 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
17439 		as of the June 2, 2021 release.
17440 
17441 		History:
17442 			Added June 2, 2021.
17443 
17444 		See_Also: [getPropertyAsString], [setPropertyFromString]
17445 	+/
17446 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
17447 	/++
17448 		Requests a property to be delivered to you as a string, through your `sink` delegate.
17449 
17450 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
17451 		be interpreted as json, otherwise, it is just a plain string.
17452 
17453 		The sink should always be called exactly once for each call (it is basically a return value, but it might
17454 		use a local buffer it maintains instead of allocating a return value).
17455 
17456 		History:
17457 			Added June 2, 2021.
17458 
17459 		See_Also: [getPropertiesList], [setPropertyFromString]
17460 	+/
17461 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
17462 	/++
17463 		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.
17464 
17465 		History:
17466 			Added June 2, 2021.
17467 
17468 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
17469 	+/
17470 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
17471 
17472 	/// [setPropertyFromString] possible return values
17473 	enum SetPropertyResult {
17474 		success = 0, /// the property has been successfully set to the request value
17475 		notPermitted = -1, /// the property exists but it cannot be changed at this time
17476 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
17477 		noSuchProperty = -3, /// there is no property by that name
17478 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
17479 		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)
17480 	}
17481 
17482 	/++
17483 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
17484 
17485 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
17486 
17487 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
17488 		rarely need to use these building blocks directly.
17489 	+/
17490 	mixin template RegisterSetters() {
17491 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
17492 			switch(name) {
17493 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
17494 					case memberName:
17495 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
17496 							if(value != "true" && value != "false")
17497 								return SetPropertyResult.wrongFormat;
17498 							__traits(getMember, this, memberName) = value == "true" ? true : false;
17499 							return SetPropertyResult.success;
17500 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
17501 							import core.stdc.stdlib;
17502 							char[128] zero = 0;
17503 							if(buffer.length + 1 >= zero.length)
17504 								return SetPropertyResult.wrongFormat;
17505 							zero[0 .. buffer.length] = buffer[];
17506 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
17507 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
17508 							import core.stdc.stdlib;
17509 							char[128] zero = 0;
17510 							if(buffer.length + 1 >= zero.length)
17511 								return SetPropertyResult.wrongFormat;
17512 							zero[0 .. buffer.length] = buffer[];
17513 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
17514 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
17515 							__traits(getMember, this, memberName) = value.idup;
17516 						} else {
17517 							return SetPropertyResult.notImplemented;
17518 						}
17519 
17520 				}
17521 				default:
17522 					return super.setPropertyFromString(name, value, valueIsJson);
17523 			}
17524 		}
17525 	}
17526 
17527 	/++
17528 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
17529 
17530 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
17531 
17532 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
17533 		rarely need to use these building blocks directly.
17534 	+/
17535 	mixin template RegisterGetters() {
17536 		override void getPropertiesList(scope void delegate(string name) sink) const {
17537 			super.getPropertiesList(sink);
17538 
17539 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
17540 				sink(memberName);
17541 			}
17542 		}
17543 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
17544 			switch(name) {
17545 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
17546 					case memberName:
17547 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
17548 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
17549 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
17550 							import core.stdc.stdio;
17551 							char[32] buffer;
17552 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
17553 							sink(name, buffer[0 .. len], true);
17554 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
17555 							import core.stdc.stdio;
17556 							char[32] buffer;
17557 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
17558 							sink(name, buffer[0 .. len], true);
17559 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
17560 							sink(name, __traits(getMember, this, memberName), false);
17561 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
17562 						} else {
17563 							sink(name, null, true);
17564 						}
17565 
17566 					return;
17567 				}
17568 				default:
17569 					return super.getPropertyAsString(name, sink);
17570 			}
17571 		}
17572 	}
17573 }
17574 
17575 private struct Stack(T) {
17576 	this(int maxSize) {
17577 		internalLength = 0;
17578 		arr = initialBuffer[];
17579 	}
17580 
17581 	///.
17582 	void push(T t) {
17583 		if(internalLength >= arr.length) {
17584 			auto oldarr = arr;
17585 			if(arr.length < 4096)
17586 				arr = new T[arr.length * 2];
17587 			else
17588 				arr = new T[arr.length + 4096];
17589 			arr[0 .. oldarr.length] = oldarr[];
17590 		}
17591 
17592 		arr[internalLength] = t;
17593 		internalLength++;
17594 	}
17595 
17596 	///.
17597 	T pop() {
17598 		assert(internalLength);
17599 		internalLength--;
17600 		return arr[internalLength];
17601 	}
17602 
17603 	///.
17604 	T peek() {
17605 		assert(internalLength);
17606 		return arr[internalLength - 1];
17607 	}
17608 
17609 	///.
17610 	@property bool empty() {
17611 		return internalLength ? false : true;
17612 	}
17613 
17614 	///.
17615 	private T[] arr;
17616 	private size_t internalLength;
17617 	private T[64] initialBuffer;
17618 	// 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),
17619 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
17620 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
17621 }
17622 
17623 /// 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.
17624 private struct WidgetStream {
17625 
17626 	///.
17627 	@property Widget front() {
17628 		return current.widget;
17629 	}
17630 
17631 	/// Use Widget.tree instead.
17632 	this(Widget start) {
17633 		current.widget = start;
17634 		current.childPosition = -1;
17635 		isEmpty = false;
17636 		stack = typeof(stack)(0);
17637 	}
17638 
17639 	/*
17640 		Handle it
17641 		handle its children
17642 
17643 	*/
17644 
17645 	///.
17646 	void popFront() {
17647 	    more:
17648 	    	if(isEmpty) return;
17649 
17650 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
17651 
17652 		current.childPosition++;
17653 		if(current.childPosition >= current.widget.children.length) {
17654 			if(stack.empty())
17655 				isEmpty = true;
17656 			else {
17657 				current = stack.pop();
17658 				goto more;
17659 			}
17660 		} else {
17661 			stack.push(current);
17662 			current.widget = current.widget.children[current.childPosition];
17663 			current.childPosition = -1;
17664 		}
17665 	}
17666 
17667 	///.
17668 	@property bool empty() {
17669 		return isEmpty;
17670 	}
17671 
17672 	private:
17673 
17674 	struct Current {
17675 		Widget widget;
17676 		int childPosition;
17677 	}
17678 
17679 	Current current;
17680 
17681 	Stack!(Current) stack;
17682 
17683 	bool isEmpty;
17684 }
17685 
17686 
17687 /+
17688 
17689 	I could fix up the hierarchy kinda like this
17690 
17691 	class Widget {
17692 		Widget[] children() { return null; }
17693 	}
17694 	interface WidgetContainer {
17695 		Widget asWidget();
17696 		void addChild(Widget w);
17697 
17698 		// alias asWidget this; // but meh
17699 	}
17700 
17701 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
17702 
17703 	class Layout : Widget, WidgetContainer {}
17704 
17705 	class Window : WidgetContainer {}
17706 
17707 
17708 	All constructors that previously took Widgets should now take WidgetContainers instead
17709 
17710 
17711 
17712 	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".
17713 +/
17714 
17715 /+
17716 	LAYOUTS 2.0
17717 
17718 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
17719 
17720 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
17721 
17722 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
17723 
17724 	and even Paint can just use computedStyle...
17725 
17726 		background color
17727 		font
17728 		border color and style
17729 
17730 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
17731 		please note that many widgets and in some modes will completely ignore properties as they will.
17732 		they are just hints you set, not promises.
17733 
17734 
17735 
17736 
17737 
17738 	So generally the existing virtual functions are just the default for the class. But individual objects
17739 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
17740 +/
17741 
17742 /++
17743 	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.
17744 
17745 	History:
17746 		Added May 24, 2021.
17747 +/
17748 struct WidgetBackground {
17749 	/++
17750 		A background with the given solid color.
17751 	+/
17752 	this(Color color) {
17753 		this.color = color;
17754 	}
17755 
17756 	this(WidgetBackground bg) {
17757 		this = bg;
17758 	}
17759 
17760 	/++
17761 		Creates a widget from the string.
17762 
17763 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
17764 	+/
17765 	static WidgetBackground fromString(string s) {
17766 		return WidgetBackground(Color.fromString(s));
17767 	}
17768 
17769 	/++
17770 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
17771 
17772 		History:
17773 			Made `public` on December 18, 2022 (dub v10.10).
17774 	+/
17775 	Color color;
17776 }
17777 
17778 /++
17779 	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!)
17780 
17781 	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.
17782 
17783 	You should not inherit from this directly, but instead use [VisualTheme].
17784 
17785 	History:
17786 		Added May 8, 2021
17787 +/
17788 abstract class BaseVisualTheme {
17789 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
17790 	abstract void doPaint(Widget widget, WidgetPainter painter);
17791 
17792 	/+
17793 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
17794 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
17795 	+/
17796 
17797 	/++
17798 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
17799 		where the interpretation of the string varies for each property and may include things like measurement units.
17800 	+/
17801 	abstract string getPropertyString(Widget widget, string propertyName);
17802 
17803 	/++
17804 		Default background color of the window. Widgets also use this to simulate transparency.
17805 
17806 		Probably some shade of grey.
17807 	+/
17808 	abstract Color windowBackgroundColor();
17809 	abstract Color widgetBackgroundColor();
17810 	abstract Color foregroundColor();
17811 	abstract Color lightAccentColor();
17812 	abstract Color darkAccentColor();
17813 
17814 	/++
17815 		Colors used to indicate active selections in lists and text boxes, etc.
17816 	+/
17817 	abstract Color selectionForegroundColor();
17818 	/// ditto
17819 	abstract Color selectionBackgroundColor();
17820 
17821 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
17822 
17823 	/++
17824 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
17825 	+/
17826 	abstract OperatingSystemFont defaultFont(int dpi);
17827 
17828 	private OperatingSystemFont[int] defaultFontCache_;
17829 	private OperatingSystemFont defaultFontCached(int dpi) {
17830 		if(dpi !in defaultFontCache_) {
17831 			// FIXME: set this to false if X disconnect or if visual theme changes
17832 			defaultFontCache_[dpi] = defaultFont(dpi);
17833 		}
17834 		return defaultFontCache_[dpi];
17835 	}
17836 }
17837 
17838 /+
17839 	A widget should have:
17840 		classList
17841 		dataset
17842 		attributes
17843 		computedStyles
17844 		state (persistent)
17845 		dynamic state (focused, hover, etc)
17846 +/
17847 
17848 // visualTheme.computedStyle(this).paddingLeft
17849 
17850 
17851 /++
17852 	This is your entry point to create your own visual theme for custom widgets.
17853 
17854 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
17855 
17856 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
17857 +/
17858 abstract class VisualTheme(CRTP) : BaseVisualTheme {
17859 	override string getPropertyString(Widget widget, string propertyName) {
17860 		return null;
17861 	}
17862 
17863 	/+
17864 		mixin StyleOverride!Widget
17865 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
17866 		w.useStyleProperties(dg);
17867 	}
17868 	+/
17869 
17870 	final override void doPaint(Widget widget, WidgetPainter painter) {
17871 		auto derived = cast(CRTP) cast(void*) this;
17872 
17873 		scope void delegate(Widget, WidgetPainter) bestMatch;
17874 		int bestMatchScore;
17875 
17876 		static if(__traits(hasMember, CRTP, "paint"))
17877 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
17878 			static if(is(typeof(overload) Params == __parameters)) {
17879 				static assert(Params.length == 2);
17880 				static assert(is(Params[0] : Widget));
17881 				static assert(is(Params[1] == WidgetPainter));
17882 				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);
17883 
17884 				alias type = Params[0];
17885 				if(cast(type) widget) {
17886 					auto score = baseClassCount!type;
17887 
17888 					if(score > bestMatchScore) {
17889 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
17890 						bestMatchScore = score;
17891 					}
17892 				}
17893 			} else static assert(0, "paint should be a method.");
17894 		}
17895 
17896 		if(bestMatch)
17897 			bestMatch(widget, painter);
17898 		else
17899 			widget.paint(painter);
17900 	}
17901 
17902 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
17903 
17904 	// 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
17905 	// mixin Beautiful95Theme;
17906 	mixin DefaultLightTheme;
17907 
17908 	private static struct Cached {
17909 		// i prolly want to do this
17910 	}
17911 }
17912 
17913 /// ditto
17914 mixin template Beautiful95Theme() {
17915 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
17916 	override Color widgetBackgroundColor() { return Color.white; }
17917 	override Color foregroundColor() { return Color.black; }
17918 	override Color darkAccentColor() { return Color(172, 172, 172); }
17919 	override Color lightAccentColor() { return Color(223, 223, 223); }
17920 	override Color selectionForegroundColor() { return Color.white; }
17921 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
17922 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
17923 }
17924 
17925 /// ditto
17926 mixin template DefaultLightTheme() {
17927 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
17928 	override Color widgetBackgroundColor() { return Color.white; }
17929 	override Color foregroundColor() { return Color.black; }
17930 	override Color darkAccentColor() { return Color(172, 172, 172); }
17931 	override Color lightAccentColor() { return Color(223, 223, 223); }
17932 	override Color selectionForegroundColor() { return Color.white; }
17933 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
17934 	override OperatingSystemFont defaultFont(int dpi) {
17935 		version(Windows)
17936 			return new OperatingSystemFont("Segoe UI");
17937 		else static if(UsingSimpledisplayCocoa) {
17938 			return (new OperatingSystemFont()).loadDefault;
17939 		} else {
17940 			// FIXME: undo xft's scaling so we don't end up double scaled
17941 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
17942 		}
17943 	}
17944 }
17945 
17946 /// ditto
17947 mixin template DefaultDarkTheme() {
17948 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
17949 	override Color widgetBackgroundColor() { return Color.black; }
17950 	override Color foregroundColor() { return Color.white; }
17951 	override Color darkAccentColor() { return Color(20, 20, 20); }
17952 	override Color lightAccentColor() { return Color(80, 80, 80); }
17953 	override Color selectionForegroundColor() { return Color.white; }
17954 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
17955 	override OperatingSystemFont defaultFont(int dpi) {
17956 		version(Windows)
17957 			return new OperatingSystemFont("Segoe UI", 12);
17958 		else static if(UsingSimpledisplayCocoa) {
17959 			return (new OperatingSystemFont()).loadDefault;
17960 		} else {
17961 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
17962 		}
17963 	}
17964 }
17965 
17966 /// ditto
17967 alias DefaultTheme = DefaultLightTheme;
17968 
17969 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
17970 	/+
17971 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
17972 	Color windowBackgroundColor() { return Color(242, 242, 242); }
17973 	Color darkAccentColor() { return windowBackgroundColor; }
17974 	Color lightAccentColor() { return windowBackgroundColor; }
17975 	+/
17976 }
17977 
17978 /++
17979 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
17980 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
17981 
17982 	History:
17983 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
17984 +/
17985 class StateChanged(alias field) : Event {
17986 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
17987 	override bool cancelable() const { return false; }
17988 	this(Widget target, typeof(field) newValue) {
17989 		this.newValue = newValue;
17990 		super(EventString, target);
17991 	}
17992 
17993 	typeof(field) newValue;
17994 }
17995 
17996 /++
17997 	Convenience function to add a `triggered` event listener.
17998 
17999 	Its implementation is simply `w.addEventListener("triggered", dg);`
18000 
18001 	History:
18002 		Added November 27, 2021 (dub v10.4)
18003 +/
18004 void addWhenTriggered(Widget w, void delegate() dg) {
18005 	w.addEventListener("triggered", dg);
18006 }
18007 
18008 /++
18009 	Observable varables can be added to widgets and when they are changed, it fires
18010 	off a [StateChanged] event so you can react to it.
18011 
18012 	It is implemented as a getter and setter property, along with another helper you
18013 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
18014 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
18015 	example.
18016 
18017 	History:
18018 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
18019 +/
18020 mixin template Observable(T, string name) {
18021 	private T backing;
18022 
18023 	mixin(q{
18024 		void } ~ name ~ q{_changed (void delegate(T) dg) {
18025 			this.addEventListener((StateChanged!this_thing ev) {
18026 				dg(ev.newValue);
18027 			});
18028 		}
18029 
18030 		@property T } ~ name ~ q{ () {
18031 			return backing;
18032 		}
18033 
18034 		@property void } ~ name ~ q{ (T t) {
18035 			backing = t;
18036 			auto event = new StateChanged!this_thing(this, t);
18037 			event.dispatch();
18038 		}
18039 	});
18040 
18041 	mixin("private alias this_thing = " ~ name ~ ";");
18042 }
18043 
18044 
18045 private bool startsWith(string test, string thing) {
18046 	if(test.length < thing.length)
18047 		return false;
18048 	return test[0 .. thing.length] == thing;
18049 }
18050 
18051 private bool endsWith(string test, string thing) {
18052 	if(test.length < thing.length)
18053 		return false;
18054 	return test[$ - thing.length .. $] == thing;
18055 }
18056 
18057 /++
18058 	Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon`
18059 
18060 	Note they can NOT have accelerators or toolbars; those annotations will be ignored.
18061 
18062 	Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time.
18063 
18064 	See_Also:
18065 		[Widget.setMenuAndToolbarFromAnnotatedCode]
18066 +/
18067 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) {
18068 	return createContextMenuFromAnnotatedCode(w, w);
18069 }
18070 
18071 /// ditto
18072 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) {
18073 	return createContextMenuFromAnnotatedCode_internal(w, t);
18074 }
18075 /// ditto
18076 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) {
18077 	return createContextMenuFromAnnotatedCode_internal(w, t);
18078 }
18079 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) {
18080 	Menu ret = new Menu("", w);
18081 
18082 	foreach(memberName; __traits(derivedMembers, T)) {
18083 		static if(memberName != "this")
18084 		static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
18085 			.menu menu;
18086 			bool separator;
18087 			.hotkey hotkey;
18088 			.icon icon;
18089 			string label;
18090 			string tip;
18091 			foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
18092 				static if(is(typeof(attr) == .menu))
18093 					menu = attr;
18094 				else static if(is(attr == .separator))
18095 					separator = true;
18096 				else static if(is(typeof(attr) == .hotkey))
18097 					hotkey = attr;
18098 				else static if(is(typeof(attr) == .icon))
18099 					icon = attr;
18100 				else static if(is(typeof(attr) == .label))
18101 					label = attr.label;
18102 				else static if(is(typeof(attr) == .tip))
18103 					tip = attr.tip;
18104 			}
18105 
18106 			if(menu is .menu.init) {
18107 				ushort correctIcon = icon.id; // FIXME
18108 				if(label.length == 0)
18109 					label = memberName.toMenuLabel;
18110 
18111 				auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName));
18112 
18113 				auto action = new Action(label, correctIcon, handler);
18114 
18115 				if(separator)
18116 					ret.addSeparator();
18117 					ret.addItem(new MenuItem(action));
18118 			}
18119 		}
18120 	}
18121 
18122 	return ret;
18123 }
18124 
18125 // still do layout delegation
18126 // and... split off Window from Widget.
18127 
18128 version(minigui_screenshots)
18129 struct Screenshot {
18130 	string name;
18131 }
18132 
18133 version(minigui_screenshots)
18134 static if(__VERSION__ > 2092)
18135 mixin(q{
18136 shared static this() {
18137 	import core.runtime;
18138 
18139 	static UnitTestResult screenshotMagic() {
18140 		string name;
18141 
18142 		import arsd.png;
18143 
18144 		auto results = new Window();
18145 		auto button = new Button("do it", results);
18146 
18147 		Window.newWindowCreated = delegate(Window w) {
18148 			Timer timer;
18149 			timer = new Timer(250, {
18150 				auto img = w.win.takeScreenshot();
18151 				timer.destroy();
18152 
18153 				version(Windows)
18154 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
18155 				else
18156 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
18157 
18158 				w.close();
18159 			});
18160 		};
18161 
18162 		button.addWhenTriggered( {
18163 
18164 		foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) {
18165 			name = null;
18166 			static foreach(attr; __traits(getAttributes, test)) {
18167 				static if(is(typeof(attr) == Screenshot))
18168 					name = attr.name;
18169 			}
18170 			if(name.length) {
18171 				test();
18172 			}
18173 		}
18174 
18175 		});
18176 
18177 		results.loop();
18178 
18179 		return UnitTestResult(0, 0, false, false);
18180 	}
18181 
18182 
18183 	Runtime.extendedModuleUnitTester = &screenshotMagic;
18184 }
18185 });
18186 version(minigui_screenshots) {
18187 	version(unittest)
18188 		void main() {}
18189 	else static assert(0, "dont forget the -unittest flag to dmd");
18190 }
18191 
18192 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
18193 // FIXME: make multiple accelerators disambiguate based ona rgs
18194 // FIXME: MainWindow ctor should have same arg order as Window
18195 // FIXME: mainwindow ctor w/ client area size instead of total size.
18196 // 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.
18197 // FIXME: tri-state checkbox
18198 // FIXME: subordinate controls grouping...